diff --git a/ios/Platform/Platform/ContentView.swift b/ios/Platform/Platform/ContentView.swift index 63c8127..aa6a590 100644 --- a/ios/Platform/Platform/ContentView.swift +++ b/ios/Platform/Platform/ContentView.swift @@ -85,178 +85,108 @@ struct MainTabView: View { } } -// MARK: - Confetti (canvas-confetti style: burst from bottom-right) +// MARK: - Confetti (ReactFlux style: ceiling drop, full screen scatter) struct ConfettiView: View { - @State private var particles: [Particle] = [] - @State private var showCheck = false + @State private var particles: [ConfettiPiece] = [] - static let palette: [Color] = [ - .red, .orange, .yellow, .green, .blue, .purple, .pink, - Color(red: 1, green: 0.84, blue: 0), // gold - Color(red: 0, green: 0.8, blue: 0.6), // teal - Color(red: 0.93, green: 0.35, blue: 0.35), // coral - Color(red: 0.58, green: 0.44, blue: 0.86), // violet - Color(red: 1, green: 0.6, blue: 0.7), // pink + static let colors: [Color] = [ + Color(red: 1, green: 0.22, blue: 0.36), // hot pink + Color(red: 0.26, green: 0.63, blue: 1), // sky blue + Color(red: 1, green: 0.84, blue: 0), // gold + Color(red: 0.18, green: 0.8, blue: 0.44), // green + Color(red: 0.61, green: 0.35, blue: 0.96), // purple + Color(red: 1, green: 0.55, blue: 0), // orange + Color(red: 0, green: 0.82, blue: 0.77), // teal + Color(red: 1, green: 0.41, blue: 0.71), // pink + Color(red: 0.39, green: 0.4, blue: 1), // indigo + Color(red: 1, green: 0.92, blue: 0.23), // yellow ] - struct Particle: Identifiable { + struct ConfettiPiece: Identifiable { let id = UUID() let color: Color - let size: CGFloat - let shape: Int - // Start position (bottom-right area) - let startX: CGFloat + let width: CGFloat + let height: CGFloat + let x: CGFloat let startY: CGFloat - // Velocity - let vx: CGFloat - let vy: CGFloat + let drift: CGFloat let spin: Double let delay: Double + let fallDuration: Double } var body: some View { GeometryReader { geo in ZStack { ForEach(particles) { p in - particleView(p) - .modifier(PhysicsModifier( - startX: p.startX, startY: p.startY, - vx: p.vx, vy: p.vy, - spin: p.spin, delay: p.delay, - bounds: geo.size + RoundedRectangle(cornerRadius: 1) + .fill(p.color) + .frame(width: p.width, height: p.height) + .modifier(CeilingDropModifier( + x: p.x, startY: p.startY, + endY: geo.size.height + 40, + drift: p.drift, spin: p.spin, + delay: p.delay, duration: p.fallDuration )) } - - // Success check - if showCheck { - VStack(spacing: 6) { - Image(systemName: "checkmark.circle.fill") - .font(.system(size: 50)) - .foregroundStyle(.white) - .shadow(color: .black.opacity(0.3), radius: 10) - Text("Added!") - .font(.subheadline.weight(.bold)) - .foregroundStyle(.white) - .shadow(color: .black.opacity(0.3), radius: 4) - } - .transition(.scale.combined(with: .opacity)) - } } - .onAppear { generate(size: geo.size) } + .onAppear { scatter(in: geo.size) } } } - @ViewBuilder - private func particleView(_ p: Particle) -> some View { - Group { - switch p.shape % 3 { - case 0: Circle().fill(p.color) - case 1: Rectangle().fill(p.color) - default: Star(corners: 5, smoothness: 0.45).fill(p.color) - } - } - .frame(width: p.size, height: p.shape % 3 == 1 ? p.size * 0.6 : p.size) - } - - private func generate(size: CGSize) { - let originX = size.width * 0.9 - let originY = size.height * 0.85 - - // canvas-confetti style: angle=120, spread=70, particleCount=100 - // angle 120° means shooting upper-left from bottom-right - let baseAngle: Double = 120 - let spread: Double = 70 - - particles = (0..<100).map { _ in - let angle = baseAngle + Double.random(in: -spread/2...spread/2) - let radians = angle * .pi / 180 - let speed = CGFloat.random(in: 15...45) - - return Particle( - color: Self.palette.randomElement()!, - size: CGFloat.random(in: 5...11), - shape: Int.random(in: 0...2), - startX: originX + CGFloat.random(in: -20...20), - startY: originY + CGFloat.random(in: -20...20), - vx: cos(radians) * speed, - vy: -sin(radians) * speed, // negative = upward - spin: Double.random(in: -360...360), - delay: Double.random(in: 0...0.15) + private func scatter(in size: CGSize) { + particles = (0..<200).map { _ in + ConfettiPiece( + color: Self.colors.randomElement()!, + width: CGFloat.random(in: 4...8), + height: CGFloat.random(in: 6...14), + x: CGFloat.random(in: 0...size.width), + startY: CGFloat.random(in: (-200)...(-20)), + drift: CGFloat.random(in: -30...30), + spin: Double.random(in: -540...540), + delay: Double.random(in: 0...0.4), + fallDuration: Double.random(in: 1.5...2.5) ) } - - withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) { - showCheck = true - } - DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { - withAnimation(.easeOut(duration: 0.3)) { showCheck = false } - } + UINotificationFeedbackGenerator().notificationOccurred(.success) } } -// Physics: gravity pulls down, particles drift and fade -struct PhysicsModifier: ViewModifier { - let startX: CGFloat +struct CeilingDropModifier: ViewModifier { + let x: CGFloat let startY: CGFloat - let vx: CGFloat - let vy: CGFloat + let endY: CGFloat + let drift: CGFloat let spin: Double let delay: Double - let bounds: CGSize + let duration: Double - @State private var time: CGFloat = 0 + @State private var progress: CGFloat = 0 @State private var opacity: Double = 1 - private let gravity: CGFloat = 0.6 - private let duration: Double = 1.6 - func body(content: Content) -> some View { - let x = startX + vx * time - let y = startY + vy * time + 0.5 * gravity * time * time - content - .offset(x: x, y: y) + .position( + x: x + drift * progress, + y: startY + (endY - startY) * progress + ) .opacity(opacity) - .rotationEffect(Angle.degrees(spin * Double(time / 30))) + .rotation3DEffect( + Angle.degrees(spin * Double(progress)), + axis: (x: Double.random(in: 0...1), y: Double.random(in: 0...1), z: 0.5) + ) .onAppear { - withAnimation(.easeOut(duration: duration).delay(delay)) { - time = 30 + withAnimation(.easeIn(duration: duration).delay(delay)) { + progress = 1 } - withAnimation(.easeIn(duration: 0.5).delay(delay + duration * 0.7)) { + withAnimation(.easeIn(duration: 0.3).delay(delay + duration * 0.8)) { opacity = 0 } } } } -// Star shape -struct Star: Shape { - let corners: Int - let smoothness: CGFloat - - func path(in rect: CGRect) -> Path { - let center = CGPoint(x: rect.midX, y: rect.midY) - let outerRadius = min(rect.width, rect.height) / 2 - let innerRadius = outerRadius * smoothness - var path = Path() - let step = .pi / CGFloat(corners) - - for i in 0..