// // ConfettiView.swift // Tusker // // Created by Shadowfacts on 1/25/23. // Copyright © 2023 Shadowfacts. All rights reserved. // import SwiftUI struct ConfettiView: View { private static let colors = [ Color.red, .purple, .blue, .pink, .yellow, .green, .teal, .cyan, .mint, .indigo, .orange, ] @State private var size: CGSize? @State private var startDate: Date? var body: some View { allConfetti .frame(maxWidth: .infinity, maxHeight: .infinity) .background(GeometryReader { proxy in Color.clear .preference(key: SizeKey.self, value: proxy.size) .onPreferenceChange(SizeKey.self) { newValue in self.size = newValue self.startDate = Date() } }) } @ViewBuilder private var allConfetti: some View { if let size { TimelineView(.animation) { context in ZStack { ForEach(0..<200) { idx in let time = context.date.timeIntervalSince(startDate!) ConfettiPiece(shape: Rectangle(), color: Self.colors[idx % Self.colors.count], canvasSize: size, time: time) ConfettiPiece(shape: Circle(), color: Self.colors[idx % Self.colors.count], canvasSize: size, time: time) ConfettiPiece(shape: Triangle(), color: Self.colors[idx % Self.colors.count], canvasSize: size, time: time) } } } } else { Color.clear } } } private struct SizeKey: PreferenceKey { static var defaultValue: CGSize = .zero static func reduce(value: inout CGSize, nextValue: () -> CGSize) { value = nextValue() } } private struct ConfettiPiece: View { let shape: S let color: Color let canvasSize: CGSize let time: TimeInterval @State private var start: CGPoint = .zero @State private var velocity: CGPoint = { let angle = CGFloat.random(in: 0..<2) * .pi let velocity = CGFloat.random(in: 25...250) return CGPoint(x: cos(angle) * velocity, y: sin(angle) * velocity) }() private let gravity: CGFloat = 100 @State private var rotationAxis: (CGFloat, CGFloat, CGFloat) = (.random() ? 1 : 0, .random() ? 1 : 0, .random() ? 1 : 0) @State private var rotationsPerSecond = Double.random(in: 0.5..<2) private var x: CGFloat { (canvasSize.width / 2) + velocity.x * time } private var y: CGFloat { (canvasSize.height / 2) + velocity.y * time + 0.5 * time * time * gravity } private var angle: Angle { .degrees(time * rotationsPerSecond * 360) } var body: some View { shape .frame(width: 7, height: 7) .foregroundColor(color) .rotation3DEffect(angle, axis: rotationAxis) .position(x: x, y: y) } } private struct Triangle: Shape { func path(in rect: CGRect) -> Path { var p = Path() p.move(to: CGPoint(x: rect.midX, y: rect.minY)) p.addLine(to: CGPoint(x: rect.minX, y: rect.maxY)) p.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY)) p.closeSubpath() return p } } struct ConfettiView_Previews: PreviewProvider { static var previews: some View { ConfettiView() } }