120 lines
3.5 KiB
Swift
120 lines
3.5 KiB
Swift
//
|
|
// 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 let defaultValue: CGSize = .zero
|
|
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
|
|
value = nextValue()
|
|
}
|
|
}
|
|
|
|
private struct ConfettiPiece<S: Shape>: 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()
|
|
}
|
|
}
|