CasinoGames/Baccarat/Views/CardView.swift

273 lines
9.2 KiB
Swift

//
// 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: Design.Animation.springDuration, bounce: Design.Animation.cardFlipBounce), 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
// 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)
}
}
#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)
}
}
}