CasinoGames/Baccarat/Views/ResultBannerView.swift

172 lines
6.5 KiB
Swift

//
// ResultBannerView.swift
// Baccarat
//
// Animated result banner showing the winner and winnings.
//
import SwiftUI
/// 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
}
}
}
}
/// 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)
}
}
#Preview {
ZStack {
Color.Table.preview
.ignoresSafeArea()
ResultBannerView(result: .playerWins, winnings: 500)
}
}