// // ResultBannerView.swift // Baccarat // // Animated result banner showing the winner and winnings. // import SwiftUI import CasinoKit /// An animated banner showing the round result. struct ResultBannerView: View { let result: GameResult let winnings: Int @State private var showBanner = false @State private var showText = false @State private var showWinnings = false // MARK: - Scaled Font Sizes (Dynamic Type) @ScaledMetric(relativeTo: .largeTitle) private var resultFontSize: CGFloat = Design.BaseFontSize.largeTitle @ScaledMetric(relativeTo: .title2) private var winningsFontSize: CGFloat = 28 var body: some View { ZStack { // Background overlay Color.black.opacity(showBanner ? Design.Opacity.medium : 0) .ignoresSafeArea() .animation(.easeIn(duration: Design.Animation.fadeInDuration), value: showBanner) // Banner VStack(spacing: Design.Spacing.xLarge) { // Result text Text(result.displayText) .font(.system(size: resultFontSize, weight: .black, design: .rounded)) .foregroundStyle( LinearGradient( colors: [.white, result.color], startPoint: .top, endPoint: .bottom ) ) .shadow(color: result.color.opacity(Design.Opacity.heavy), radius: Design.Shadow.radiusLarge) .scaleEffect(showText ? Design.Scale.normal : Design.Scale.shrunk) .opacity(showText ? Design.Scale.normal : 0) // Winnings display if winnings != 0 { HStack(spacing: Design.Spacing.small) { if winnings > 0 { Image(systemName: "plus.circle.fill") .foregroundStyle(.green) Text("\(winnings)") .foregroundStyle(.green) } else { Image(systemName: "minus.circle.fill") .foregroundStyle(.red) Text("\(abs(winnings))") .foregroundStyle(.red) } } .font(.system(size: winningsFontSize, weight: .bold, design: .rounded)) .scaleEffect(showWinnings ? Design.Scale.normal : Design.Scale.shrunk) .opacity(showWinnings ? Design.Scale.normal : 0) } } .padding(Design.Spacing.xxxLarge + Design.Spacing.small) .background( RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge + Design.Spacing.xSmall) .fill( LinearGradient( colors: [ Color(white: 0.15), Color(white: 0.08) ], startPoint: .top, endPoint: .bottom ) ) .overlay( RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge + Design.Spacing.xSmall) .strokeBorder( LinearGradient( colors: [ result.color.opacity(Design.Opacity.heavy), result.color.opacity(Design.Opacity.light) ], startPoint: .topLeading, endPoint: .bottomTrailing ), lineWidth: Design.LineWidth.thick ) ) ) .shadow(color: result.color.opacity(Design.Opacity.light), radius: Design.Shadow.radiusXXLarge) .scaleEffect(showBanner ? Design.Scale.normal : Design.Scale.slightShrink) .opacity(showBanner ? Design.Scale.normal : 0) } .onAppear { withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce)) { showBanner = true } withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce).delay(Design.Animation.staggerDelay1)) { showText = true } withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce).delay(Design.Animation.staggerDelay2)) { showWinnings = true } // Announce result to VoiceOver users announceResult() } .accessibilityElement(children: .ignore) .accessibilityLabel(accessibilityDescription) .accessibilityAddTraits(.updatesFrequently) } // MARK: - Accessibility private var accessibilityDescription: String { var description = result.displayText if winnings > 0 { let format = String(localized: "wonAmountFormat") description += ". " + String(format: format, winnings.formatted()) } else if winnings < 0 { let format = String(localized: "lostAmountFormat") description += ". " + String(format: format, abs(winnings).formatted()) } return description } private func announceResult() { // Post accessibility announcement for screen reader users Task { @MainActor in try? await Task.sleep(for: .milliseconds(500)) AccessibilityNotification.Announcement(accessibilityDescription).post() } } } /// Confetti particle for celebrations. struct ConfettiPiece: View { let color: Color @State private var position: CGPoint = .zero @State private var rotation: Double = 0 @State private var opacity: Double = 1 private let confettiWidth: CGFloat = 8 private let confettiHeight: CGFloat = 12 var body: some View { Rectangle() .fill(color) .frame(width: confettiWidth, height: confettiHeight) .rotationEffect(.degrees(rotation)) .position(position) .opacity(opacity) .onAppear { let screenWidth = 400.0 let startX = Double.random(in: 0...screenWidth) position = CGPoint(x: startX, y: -20) withAnimation(.easeIn(duration: Double.random(in: 2...4))) { position = CGPoint( x: startX + Double.random(in: -100...100), y: 800 ) rotation = Double.random(in: 360...1080) opacity = 0 } } } } /// A confetti celebration overlay. struct ConfettiView: View { let colors: [Color] = [.red, .blue, .green, .yellow, .orange, .purple, .pink] var body: some View { ZStack { ForEach(0..<50, id: \.self) { _ in ConfettiPiece(color: colors.randomElement() ?? .yellow) } } .allowsHitTesting(false) .accessibilityHidden(true) // Decorative element } } #Preview { ZStack { Color.Table.preview .ignoresSafeArea() ResultBannerView(result: .playerWins, winnings: 500) } }