// // 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 } 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: 0.4, bounce: 0.2), value: isFaceUp) } } /// The front face of a playing card showing rank and suit. struct CardFrontView: View { let card: Card let width: CGFloat let height: CGFloat private var suitColor: Color { card.suit.isRed ? .red : .black } var body: some View { ZStack { // Card background with subtle gradient RoundedRectangle(cornerRadius: 8) .fill( LinearGradient( colors: [.white, Color(white: 0.96)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) // Card border RoundedRectangle(cornerRadius: 8) .strokeBorder( LinearGradient( colors: [Color(white: 0.8), Color(white: 0.6)], startPoint: .topLeading, endPoint: .bottomTrailing ), lineWidth: 1 ) // Card content VStack { // Top left corner HStack { VStack(spacing: 0) { Text(card.rank.symbol) .font(.system(size: width * 0.22, weight: .bold, design: .serif)) Text(card.suit.rawValue) .font(.system(size: width * 0.18)) } .foregroundStyle(suitColor) Spacer() } Spacer() // Center suit (large) Text(card.suit.rawValue) .font(.system(size: width * 0.5)) .foregroundStyle(suitColor) Spacer() // Bottom right corner (inverted) HStack { Spacer() VStack(spacing: 0) { Text(card.suit.rawValue) .font(.system(size: width * 0.18)) Text(card.rank.symbol) .font(.system(size: width * 0.22, weight: .bold, design: .serif)) } .foregroundStyle(suitColor) .rotationEffect(.degrees(180)) } } .padding(width * 0.08) } .frame(width: width, height: height) .shadow(color: .black.opacity(0.2), radius: 4, x: 2, y: 2) } } /// The back of a playing card with elegant pattern. struct CardBackView: View { let width: CGFloat let height: CGFloat 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 * 0.1) // Diamond pattern overlay DiamondPatternView() .foregroundStyle( Color.Card.diamondPattern.opacity(Design.Opacity.light) ) .padding(width * 0.12) .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 * 0.15 ) ) .frame(width: width * 0.3, height: width * 0.3) // B for Baccarat Text("B") .font(.system(size: width * 0.18, 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) } } #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) } } }