From 917964e3edd74007f98dc41c3a9d29b86ce9133e Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 26 Nov 2020 15:53:13 +0100 Subject: [PATCH] new structure --- Sources/ConfettiSwiftUI/ConfettiSwiftUI.swift | 255 +++++++++++++++++- .../ConfettiSwiftUITests.swift | 2 +- 2 files changed, 252 insertions(+), 5 deletions(-) diff --git a/Sources/ConfettiSwiftUI/ConfettiSwiftUI.swift b/Sources/ConfettiSwiftUI/ConfettiSwiftUI.swift index a55abeb..24443f3 100644 --- a/Sources/ConfettiSwiftUI/ConfettiSwiftUI.swift +++ b/Sources/ConfettiSwiftUI/ConfettiSwiftUI.swift @@ -1,7 +1,254 @@ -public struct ConfettiSwiftUI { - public var text:String +// +// ConfettiView.swift +// Confetti +// +// Created by Simon Bachmann on 24.11.20. +// + +import SwiftUI + +@available(iOS 14.0, *) +public struct ConfettiCannon: View { + @Binding var counter:Int + @ObservedObject var confettiConfig:ConfettiConfig + + @State var animate:[Bool] = [] + @State var finishedAnimationCouter = 0 + @State var firtAppear = false - public init(text:String){ - self.text = text + public init(counter:Binding, + num:Int = 20, + emojis:[String] = [String](), + includeDefaultShapes:Bool = false, + colors:[Color] = [.blue, .red, .green, .yellow, .pink, .purple, .orange], + confettiSize:CGFloat = 10.0, + rainHeight: CGFloat = 600, + fadesOut:Bool = true, + opacity:Double = 1.0, + openingAngle:Angle = .degrees(60), + closingAngle:Angle = .degrees(120), + radius:CGFloat = 300, + repetitions:Int = 0, + repetitionInterval:Double = 1.0 + + ) { + self._counter = counter + + var shapes = [AnyView]() + if(emojis.count > 0){ + for emoji in emojis{ + shapes.append(AnyView(Text("\(emoji)").font(.system(size: confettiSize)))) + } + } + + if includeDefaultShapes || emojis.count == 0{ + shapes.append(AnyView(Rectangle().frame(width: confettiSize, height: confettiSize, alignment: .center))) + shapes.append(AnyView(Circle().frame(width: confettiSize, height: confettiSize, alignment: .center))) + } + + confettiConfig = ConfettiConfig( + num: num, + shapes: shapes, + colors: colors, + confettiSize: confettiSize, + rainHeight: rainHeight, + fadesOut: fadesOut, + opacity: opacity, + openingAngle: openingAngle, + closingAngle: closingAngle, + radius: radius, + repetitions: repetitions, + repetitionInterval: repetitionInterval + ) + } + + public var body: some View { + ZStack{ + ForEach(finishedAnimationCouter.. AnyView { + return confettiConfig.shapes.randomElement()! + } + + func getColor() -> Color { + return confettiConfig.colors.randomElement()! + } + + func getSpinDirection() -> CGFloat { + let spinDirections:[CGFloat] = [-1.0, 1.0] + return spinDirections.randomElement()! + } + + var body: some View{ + ConfettiView(shape:getShape(), color:getColor(), spinDirX: getSpinDirection(), spinDirZ: getSpinDirection()) +// .frame(width: confettiConfig.confettiSize, height: confettiConfig.confettiSize, alignment: .center) + .offset(x: location.x, y: location.y) +// .scaleEffect(movement.z) + .opacity(opacity) + .onAppear(){ + withAnimation(Animation.timingCurve(0.61, 1, 0.88, 1, duration: confettiConfig.explosionAnimationDuration)) { + opacity = confettiConfig.opacity + + let randomAngle:CGFloat + if confettiConfig.openingAngle.degrees <= confettiConfig.closingAngle.degrees{ + randomAngle = CGFloat.random(in: CGFloat(confettiConfig.openingAngle.degrees)...CGFloat(confettiConfig.closingAngle.degrees)) + }else{ + 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 + + location.x = distance * cos(deg2rad(randomAngle)) + location.y = -distance * sin(deg2rad(randomAngle)) + } + + DispatchQueue.main.asyncAfter(deadline: .now() + confettiConfig.explosionAnimationDuration) { + withAnimation(Animation.timingCurve(0.12, 0, 0.39, 0, duration: confettiConfig.rainAnimationDuration)) { + location.y += confettiConfig.rainHeight + opacity = confettiConfig.fadesOut ? 0 : confettiConfig.opacity + } + } + } + } + + func deg2rad(_ number: CGFloat) -> CGFloat { + return number * CGFloat.pi / 180 + } + +} + +struct ConfettiView: View { + @State var shape: AnyView + @State var color: Color + @State var spinDirX: CGFloat + @State var spinDirZ: CGFloat + @State var firstAppear = true + + + @State var move = false +// @State var xSpeed = Double.random(in: 0.7...3) + @State var xSpeed:Double = Double.random(in: 1...2) + + @State var zSpeed = Double.random(in: 1...2) + @State var anchor = CGFloat.random(in: 0...1).rounded() + + var body: some View { + shape + .foregroundColor(color) + .rotation3DEffect(.degrees(move ? 360:0), axis: (x: spinDirX, y: 0, z: 0)) + .animation(Animation.linear(duration: xSpeed).repeatCount(10, autoreverses: false), value: move) + .rotation3DEffect(.degrees(move ? 360:0), axis: (x: 0, y: 0, z: spinDirZ), anchor: UnitPoint(x: anchor, y: anchor)) + .animation(Animation.linear(duration: zSpeed).repeatForever(autoreverses: false), value: move) + .onAppear() { + if firstAppear { + move = true + firstAppear = true + } + } + } +} + + +struct Movement{ + var x: CGFloat + var y: CGFloat + var z: CGFloat + var opacity: Double +} + + +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) { + self.num = num + self.shapes = shapes + self.colors = colors + self.confettiSize = confettiSize + self.rainHeight = rainHeight + self.fadesOut = fadesOut + self.opacity = opacity + self.openingAngle = openingAngle + self.closingAngle = closingAngle + self.radius = radius + self.repetitions = repetitions + self.repetitionInterval = repetitionInterval + self.explosionAnimationDuration = Double(radius / 1500) + self.rainAnimationDuration = Double((rainHeight + radius) / 200) + } + + @Published var num:Int + @Published var shapes:[AnyView] + @Published var colors:[Color] + @Published var confettiSize:CGFloat + @Published var rainHeight:CGFloat + @Published var fadesOut:Bool + @Published var opacity:Double + @Published var openingAngle:Angle + @Published var closingAngle:Angle + @Published var radius:CGFloat + @Published var repetitions:Int + @Published var repetitionInterval:Double + @Published var explosionAnimationDuration:Double + @Published var rainAnimationDuration:Double + + + var animationDuration:Double{ + return explosionAnimationDuration + rainAnimationDuration + } + + var openingAngleRad:CGFloat{ + return CGFloat(openingAngle.degrees) * 180 / .pi + } + + var closingAngleRad:CGFloat{ + return CGFloat(closingAngle.degrees) * 180 / .pi } } diff --git a/Tests/ConfettiSwiftUITests/ConfettiSwiftUITests.swift b/Tests/ConfettiSwiftUITests/ConfettiSwiftUITests.swift index 0e913c1..ff70e01 100644 --- a/Tests/ConfettiSwiftUITests/ConfettiSwiftUITests.swift +++ b/Tests/ConfettiSwiftUITests/ConfettiSwiftUITests.swift @@ -6,7 +6,7 @@ final class ConfettiSwiftUITests: XCTestCase { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct // results. - XCTAssertEqual(ConfettiSwiftUI().text, "Hello, World!") +// XCTAssertEqual(ConfettiSwiftUI(text: "Hello World").text, "Hello, World!") } static var allTests = [