// // 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: 8) .fill( LinearGradient( colors: [ Color(red: 0.6, green: 0.1, blue: 0.15), Color(red: 0.4, green: 0.05, blue: 0.1) ], startPoint: .topLeading, endPoint: .bottomTrailing ) ) // Border RoundedRectangle(cornerRadius: 8) .strokeBorder( LinearGradient( colors: [ Color(red: 0.9, green: 0.7, blue: 0.4), Color(red: 0.7, green: 0.5, blue: 0.2) ], startPoint: .topLeading, endPoint: .bottomTrailing ), lineWidth: 2 ) // Inner pattern area RoundedRectangle(cornerRadius: 4) .fill( LinearGradient( colors: [ Color(red: 0.5, green: 0.08, blue: 0.12), Color(red: 0.35, green: 0.04, blue: 0.08) ], startPoint: .top, endPoint: .bottom ) ) .padding(width * 0.1) // Diamond pattern overlay DiamondPatternView() .foregroundStyle( Color(red: 0.9, green: 0.7, blue: 0.4).opacity(0.3) ) .padding(width * 0.12) .clipShape(RoundedRectangle(cornerRadius: 4)) // Center emblem Circle() .fill( RadialGradient( colors: [ Color(red: 0.9, green: 0.7, blue: 0.4), Color(red: 0.7, green: 0.5, blue: 0.2) ], 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(red: 0.4, green: 0.05, blue: 0.1)) } .frame(width: width, height: height) .shadow(color: .black.opacity(0.3), radius: 4, 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: 8) .strokeBorder( Color.white.opacity(0.3), style: StrokeStyle(lineWidth: 2, dash: [8, 4]) ) .frame(width: width, height: height) } } #Preview { ZStack { Color(red: 0.0, green: 0.3, blue: 0.15) .ignoresSafeArea() HStack(spacing: 20) { 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) } } }