255 lines
8.2 KiB
Swift
255 lines
8.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: 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)
|
|
}
|
|
}
|
|
}
|
|
|