// // CardView.swift // Baccarat // // Beautiful playing card view with flip animation. // import SwiftUI /// A single playing card with elegant design and flip animation. struct CardView: View { let card: Card let isFaceUp: Bool let cardWidth: CGFloat init(card: Card, isFaceUp: Bool = true, cardWidth: CGFloat = 70) { self.card = card self.isFaceUp = isFaceUp self.cardWidth = cardWidth } private var cardHeight: CGFloat { cardWidth * 1.4 } /// Accessibility label describing the card for VoiceOver. private var accessibilityDescription: String { if isFaceUp { return "\(card.rank.accessibilityName) of \(card.suit.accessibilityName)" } else { return String(localized: "Card face down") } } var body: some View { ZStack { if isFaceUp { CardFrontView(card: card, width: cardWidth, height: cardHeight) } else { CardBackView(width: cardWidth, height: cardHeight) } } .rotation3DEffect( .degrees(isFaceUp ? 0 : 180), axis: (x: 0, y: 1, z: 0) ) .animation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.cardFlipBounce), value: isFaceUp) .accessibilityLabel(accessibilityDescription) .accessibilityAddTraits(.isImage) } } /// The front face of a playing card showing rank and suit. struct CardFrontView: View { let card: Card let width: CGFloat let height: CGFloat // MARK: - Layout Constants private let rankFontRatio: CGFloat = 0.22 private let suitFontRatio: CGFloat = 0.18 private let centerSuitFontRatio: CGFloat = 0.5 private let contentPaddingRatio: CGFloat = 0.08 private let backgroundWhite: Double = 0.96 private let borderLightGray: Double = 0.8 private let borderDarkGray: Double = 0.6 private var suitColor: Color { card.suit.isRed ? .red : .black } var body: some View { ZStack { // Card background with subtle gradient RoundedRectangle(cornerRadius: Design.CornerRadius.small) .fill( LinearGradient( colors: [.white, Color(white: backgroundWhite)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) // Card border RoundedRectangle(cornerRadius: Design.CornerRadius.small) .strokeBorder( LinearGradient( colors: [Color(white: borderLightGray), Color(white: borderDarkGray)], startPoint: .topLeading, endPoint: .bottomTrailing ), lineWidth: Design.LineWidth.thin ) // Card content VStack { // Top left corner HStack { VStack(spacing: 0) { Text(card.rank.symbol) .font(.system(size: width * rankFontRatio, weight: .bold, design: .serif)) Text(card.suit.rawValue) .font(.system(size: width * suitFontRatio)) } .foregroundStyle(suitColor) Spacer() } Spacer() // Center suit (large) Text(card.suit.rawValue) .font(.system(size: width * centerSuitFontRatio)) .foregroundStyle(suitColor) Spacer() // Bottom right corner (inverted) HStack { Spacer() VStack(spacing: 0) { Text(card.suit.rawValue) .font(.system(size: width * suitFontRatio)) Text(card.rank.symbol) .font(.system(size: width * rankFontRatio, weight: .bold, design: .serif)) } .foregroundStyle(suitColor) .rotationEffect(.degrees(180)) } } .padding(width * contentPaddingRatio) } .frame(width: width, height: height) .shadow(color: .black.opacity(Design.Opacity.hint), radius: Design.Shadow.radiusSmall, x: 2, y: 2) } } /// The back of a playing card with elegant pattern. struct CardBackView: View { let width: CGFloat let height: CGFloat // MARK: - Layout Constants private let innerPaddingRatio: CGFloat = 0.1 private let patternPaddingRatio: CGFloat = 0.12 private let emblemGradientRatio: CGFloat = 0.15 private let emblemSizeRatio: CGFloat = 0.3 private let logoFontRatio: CGFloat = 0.18 var body: some View { ZStack { // Base RoundedRectangle(cornerRadius: Design.CornerRadius.small) .fill( LinearGradient( colors: [ Color.Card.backDark, Color.Card.backLight ], startPoint: .topLeading, endPoint: .bottomTrailing ) ) // Border RoundedRectangle(cornerRadius: Design.CornerRadius.small) .strokeBorder( LinearGradient( colors: [ Color.Card.patternLight, Color.Card.patternDark ], startPoint: .topLeading, endPoint: .bottomTrailing ), lineWidth: Design.LineWidth.medium ) // Inner pattern area RoundedRectangle(cornerRadius: Design.CornerRadius.small / 2) .fill( LinearGradient( colors: [ Color.Card.innerDark, Color.Card.innerLight ], startPoint: .top, endPoint: .bottom ) ) .padding(width * innerPaddingRatio) // Diamond pattern overlay DiamondPatternView() .foregroundStyle( Color.Card.diamondPattern.opacity(Design.Opacity.light) ) .padding(width * patternPaddingRatio) .clipShape(RoundedRectangle(cornerRadius: Design.CornerRadius.small / 2)) // Center emblem Circle() .fill( RadialGradient( colors: [ Color.Card.patternLight, Color.Card.patternDark ], center: .center, startRadius: 0, endRadius: width * emblemGradientRatio ) ) .frame(width: width * emblemSizeRatio, height: width * emblemSizeRatio) // B for Baccarat Text("B") .font(.system(size: width * logoFontRatio, weight: .bold, design: .serif)) .foregroundStyle(Color.Card.logoText) } .frame(width: width, height: height) .shadow(color: .black.opacity(Design.Opacity.light), radius: Design.Shadow.radiusSmall, x: 2, y: 2) } } /// A decorative diamond pattern for card backs. struct DiamondPatternView: View { var body: some View { Canvas { context, size in let spacing: CGFloat = 12 let diamondSize: CGFloat = 6 for row in stride(from: 0, to: size.height, by: spacing) { let offset = Int(row / spacing) % 2 == 0 ? 0 : spacing / 2 for col in stride(from: offset, to: size.width, by: spacing) { let path = Path { p in p.move(to: CGPoint(x: col, y: row - diamondSize / 2)) p.addLine(to: CGPoint(x: col + diamondSize / 2, y: row)) p.addLine(to: CGPoint(x: col, y: row + diamondSize / 2)) p.addLine(to: CGPoint(x: col - diamondSize / 2, y: row)) p.closeSubpath() } context.fill(path, with: .foreground) } } } } } /// A placeholder for an empty card slot. struct CardPlaceholderView: View { let width: CGFloat private var height: CGFloat { width * 1.4 } var body: some View { RoundedRectangle(cornerRadius: Design.CornerRadius.small) .strokeBorder( Color.white.opacity(Design.Opacity.light), style: StrokeStyle(lineWidth: Design.LineWidth.medium, dash: [8, 4]) ) .frame(width: width, height: height) .accessibilityLabel(String(localized: "Empty card slot")) } } #Preview { ZStack { Color.Table.preview .ignoresSafeArea() HStack(spacing: Design.Spacing.xLarge) { CardView(card: Card(suit: .hearts, rank: .ace), isFaceUp: true) CardView(card: Card(suit: .spades, rank: .king), isFaceUp: true) CardView(card: Card(suit: .diamonds, rank: .seven), isFaceUp: false) } } }