Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
03ee03b876
commit
fa5d9f4c75
@ -45,6 +45,8 @@
|
||||
// - BettingZone
|
||||
|
||||
// MARK: - Cards (additional)
|
||||
// - CardRenderer (PDF-based vector rendering for card faces)
|
||||
// - CardHand, CardFan, CardsGrid (layout views)
|
||||
// - HandDisplayView
|
||||
|
||||
// MARK: - Settings
|
||||
|
||||
@ -142,5 +142,25 @@ public struct Card: Identifiable, Equatable, Sendable {
|
||||
public var accessibilityDescription: String {
|
||||
"\(rank.accessibilityName) of \(suit.accessibilityName)"
|
||||
}
|
||||
|
||||
/// The PDF page index (0-51) for this card.
|
||||
/// PDF order: Spades (0-12), Hearts (13-25), Clubs (26-38), Diamonds (39-51)
|
||||
/// Each suit: 2,3,4,5,6,7,8,9,10,J,Q,K,A
|
||||
public var pdfIndex: Int {
|
||||
let suitOffset: Int
|
||||
switch suit {
|
||||
case .spades: suitOffset = 0
|
||||
case .hearts: suitOffset = 13
|
||||
case .clubs: suitOffset = 26
|
||||
case .diamonds: suitOffset = 39
|
||||
}
|
||||
// PDF order: 2=0, 3=1, ..., 10=8, J=9, Q=10, K=11, A=12
|
||||
let rankOffset: Int
|
||||
switch rank {
|
||||
case .ace: rankOffset = 12 // Ace is last in PDF
|
||||
default: rankOffset = rank.rawValue - 2 // 2=0, 3=1, etc.
|
||||
}
|
||||
return suitOffset + rankOffset
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BIN
CasinoKit/Sources/CasinoKit/Resources/Cards.pdf
Normal file
BIN
CasinoKit/Sources/CasinoKit/Resources/Cards.pdf
Normal file
Binary file not shown.
202
CasinoKit/Sources/CasinoKit/Views/Cards/CardLayouts.swift
Normal file
202
CasinoKit/Sources/CasinoKit/Views/Cards/CardLayouts.swift
Normal file
@ -0,0 +1,202 @@
|
||||
//
|
||||
// CardLayouts.swift
|
||||
// CasinoKit
|
||||
//
|
||||
// Layout views for displaying multiple cards (hand, fan, grid).
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Card Hand
|
||||
|
||||
/// A horizontal scrolling row of cards, like a hand in a card game.
|
||||
public struct CardHand: View {
|
||||
/// The cards in the hand.
|
||||
public let cards: [Card]
|
||||
|
||||
/// Whether each card is face up.
|
||||
public var faceUp: Bool
|
||||
|
||||
/// The width of each card.
|
||||
public var cardWidth: CGFloat
|
||||
|
||||
/// The overlap amount between cards (negative for overlap, positive for spacing).
|
||||
public var overlap: CGFloat
|
||||
|
||||
/// Creates a card hand view.
|
||||
/// - Parameters:
|
||||
/// - cards: The cards to display.
|
||||
/// - faceUp: Whether cards are face up (default: true).
|
||||
/// - cardWidth: Width of each card (default: 80).
|
||||
/// - overlap: Overlap between cards, negative for overlap (default: -30).
|
||||
public init(
|
||||
cards: [Card],
|
||||
faceUp: Bool = true,
|
||||
cardWidth: CGFloat = CasinoDesign.Size.cardWidth,
|
||||
overlap: CGFloat = -CasinoDesign.Spacing.xLarge
|
||||
) {
|
||||
self.cards = cards
|
||||
self.faceUp = faceUp
|
||||
self.cardWidth = cardWidth
|
||||
self.overlap = overlap
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: overlap) {
|
||||
ForEach(Array(cards.enumerated()), id: \.element.id) { index, card in
|
||||
CardView(card: card, isFaceUp: faceUp, cardWidth: cardWidth)
|
||||
.zIndex(Double(index))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, CasinoDesign.Spacing.medium)
|
||||
.padding(.vertical, CasinoDesign.Spacing.small)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Card Fan
|
||||
|
||||
/// A fan-shaped arrangement of cards.
|
||||
public struct CardFan: View {
|
||||
/// The cards to display.
|
||||
public let cards: [Card]
|
||||
|
||||
/// Whether each card is face up.
|
||||
public var faceUp: Bool
|
||||
|
||||
/// The width of each card.
|
||||
public var cardWidth: CGFloat
|
||||
|
||||
/// The total angle span of the fan in degrees.
|
||||
public var angleSpan: Double
|
||||
|
||||
/// Creates a fan of cards.
|
||||
/// - Parameters:
|
||||
/// - cards: The cards to display.
|
||||
/// - faceUp: Whether cards are face up (default: true).
|
||||
/// - cardWidth: Width of each card (default: 100).
|
||||
/// - angleSpan: Total angle span in degrees (default: 60).
|
||||
public init(
|
||||
cards: [Card],
|
||||
faceUp: Bool = true,
|
||||
cardWidth: CGFloat = CasinoDesign.Size.cardWidthLarge,
|
||||
angleSpan: Double = 60
|
||||
) {
|
||||
self.cards = cards
|
||||
self.faceUp = faceUp
|
||||
self.cardWidth = cardWidth
|
||||
self.angleSpan = angleSpan
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ZStack {
|
||||
ForEach(Array(cards.enumerated()), id: \.element.id) { index, card in
|
||||
let angle = angleForCard(at: index)
|
||||
|
||||
CardView(card: card, isFaceUp: faceUp, cardWidth: cardWidth)
|
||||
.rotationEffect(.degrees(angle), anchor: .bottom)
|
||||
.offset(y: -cardWidth * 0.3)
|
||||
.zIndex(Double(index))
|
||||
}
|
||||
}
|
||||
.frame(height: cardWidth * CasinoDesign.Size.cardAspectRatio + CasinoDesign.Spacing.xLarge)
|
||||
}
|
||||
|
||||
private func angleForCard(at index: Int) -> Double {
|
||||
guard cards.count > 1 else { return 0 }
|
||||
let step = angleSpan / Double(cards.count - 1)
|
||||
return -angleSpan / 2 + step * Double(index)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Cards Grid
|
||||
|
||||
/// A grid view displaying cards.
|
||||
public struct CardsGrid: View {
|
||||
/// The cards to display.
|
||||
public let cards: [Card]
|
||||
|
||||
/// Whether each card is face up.
|
||||
public var faceUp: Bool
|
||||
|
||||
/// The minimum width for each card.
|
||||
public var minCardWidth: CGFloat
|
||||
|
||||
/// Spacing between cards.
|
||||
public var spacing: CGFloat
|
||||
|
||||
/// Creates a grid showing specific cards.
|
||||
/// - Parameters:
|
||||
/// - cards: The cards to display.
|
||||
/// - faceUp: Whether cards are face up (default: true).
|
||||
/// - minCardWidth: Minimum width for each card (default: 80).
|
||||
/// - spacing: Spacing between cards (default: 10).
|
||||
public init(
|
||||
cards: [Card],
|
||||
faceUp: Bool = true,
|
||||
minCardWidth: CGFloat = CasinoDesign.Size.cardWidth,
|
||||
spacing: CGFloat = CasinoDesign.Spacing.small
|
||||
) {
|
||||
self.cards = cards
|
||||
self.faceUp = faceUp
|
||||
self.minCardWidth = minCardWidth
|
||||
self.spacing = spacing
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ScrollView {
|
||||
LazyVGrid(
|
||||
columns: [GridItem(.adaptive(minimum: minCardWidth), spacing: spacing)],
|
||||
spacing: spacing
|
||||
) {
|
||||
ForEach(cards) { card in
|
||||
CardView(card: card, isFaceUp: faceUp, cardWidth: minCardWidth)
|
||||
}
|
||||
}
|
||||
.padding(spacing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview("Card Hand") {
|
||||
ZStack {
|
||||
Color(red: 0.05, green: 0.35, blue: 0.15)
|
||||
.ignoresSafeArea()
|
||||
|
||||
CardHand(cards: [
|
||||
Card(suit: .hearts, rank: .ace),
|
||||
Card(suit: .hearts, rank: .king),
|
||||
Card(suit: .hearts, rank: .queen),
|
||||
Card(suit: .hearts, rank: .jack),
|
||||
Card(suit: .hearts, rank: .ten)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Card Fan") {
|
||||
ZStack {
|
||||
Color(red: 0.05, green: 0.35, blue: 0.15)
|
||||
.ignoresSafeArea()
|
||||
|
||||
CardFan(cards: [
|
||||
Card(suit: .spades, rank: .ace),
|
||||
Card(suit: .hearts, rank: .king),
|
||||
Card(suit: .diamonds, rank: .queen),
|
||||
Card(suit: .clubs, rank: .jack),
|
||||
Card(suit: .spades, rank: .ten)
|
||||
])
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Cards Grid") {
|
||||
CardsGrid(cards: Suit.allCases.flatMap { suit in
|
||||
[Rank.ace, .king, .queen, .jack].map { rank in
|
||||
Card(suit: suit, rank: rank)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
143
CasinoKit/Sources/CasinoKit/Views/Cards/CardRenderer.swift
Normal file
143
CasinoKit/Sources/CasinoKit/Views/Cards/CardRenderer.swift
Normal file
@ -0,0 +1,143 @@
|
||||
//
|
||||
// CardRenderer.swift
|
||||
// CasinoKit
|
||||
//
|
||||
// Renders card images from the bundled PDF at any resolution.
|
||||
// Based on CardKit by Bret Taylor (2013), Swift conversion 2024.
|
||||
//
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#elseif canImport(AppKit)
|
||||
import AppKit
|
||||
#endif
|
||||
import CoreGraphics
|
||||
import PDFKit
|
||||
|
||||
/// Renders card images from the bundled PDF at any resolution.
|
||||
/// Cards are vector-based and can be rendered at any size without quality loss.
|
||||
public final class CardRenderer: @unchecked Sendable {
|
||||
/// Shared singleton instance.
|
||||
public static let shared = CardRenderer()
|
||||
|
||||
private var pdfDocument: CGPDFDocument?
|
||||
private var imageCache: [CacheKey: CGImage] = [:]
|
||||
private let lock = NSLock()
|
||||
|
||||
private struct CacheKey: Hashable {
|
||||
let index: Int
|
||||
let width: Int
|
||||
let height: Int
|
||||
let scale: CGFloat
|
||||
}
|
||||
|
||||
private init() {
|
||||
loadPDF()
|
||||
}
|
||||
|
||||
private func loadPDF() {
|
||||
guard let url = Bundle.module.url(forResource: "Cards", withExtension: "pdf") else {
|
||||
print("CasinoKit: Could not find Cards.pdf in bundle")
|
||||
return
|
||||
}
|
||||
pdfDocument = CGPDFDocument(url as CFURL)
|
||||
}
|
||||
|
||||
/// The aspect ratio (height / width) of a card in the PDF.
|
||||
public static let aspectRatio: CGFloat = 1.3966
|
||||
|
||||
/// Calculates the height for a given width, maintaining the card aspect ratio.
|
||||
public static func height(forWidth width: CGFloat) -> CGFloat {
|
||||
(width * aspectRatio).rounded(.up)
|
||||
}
|
||||
|
||||
/// Renders a card image at the specified size.
|
||||
/// - Parameters:
|
||||
/// - card: The card to render.
|
||||
/// - size: The size to render at.
|
||||
/// - scale: The display scale (defaults to 2.0 for retina).
|
||||
/// - Returns: A CGImage of the card, or nil if rendering fails.
|
||||
public func renderCard(_ card: Card, size: CGSize, scale: CGFloat = 2.0) -> CGImage? {
|
||||
renderPage(at: card.pdfIndex, size: size, scale: scale)
|
||||
}
|
||||
|
||||
/// Renders the card back image at the specified size.
|
||||
/// Note: The app uses a custom themed card back, but this is available
|
||||
/// if you want to use the PDF card back instead.
|
||||
/// - Parameters:
|
||||
/// - size: The size to render at.
|
||||
/// - scale: The display scale.
|
||||
/// - Returns: A CGImage of the card back, or nil if rendering fails.
|
||||
public func renderCardBack(size: CGSize, scale: CGFloat = 2.0) -> CGImage? {
|
||||
// Card back is at index 52 (page 53 in PDF)
|
||||
renderPage(at: 52, size: size, scale: scale)
|
||||
}
|
||||
|
||||
private func renderPage(at index: Int, size: CGSize, scale: CGFloat) -> CGImage? {
|
||||
guard let document = pdfDocument else { return nil }
|
||||
|
||||
let cacheKey = CacheKey(
|
||||
index: index,
|
||||
width: Int(size.width),
|
||||
height: Int(size.height),
|
||||
scale: scale
|
||||
)
|
||||
|
||||
lock.lock()
|
||||
if let cached = imageCache[cacheKey] {
|
||||
lock.unlock()
|
||||
return cached
|
||||
}
|
||||
lock.unlock()
|
||||
|
||||
// PDF pages are 1-indexed
|
||||
guard let page = document.page(at: index + 1) else { return nil }
|
||||
|
||||
let scaledSize = CGSize(
|
||||
width: size.width * scale,
|
||||
height: size.height * scale
|
||||
)
|
||||
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
guard let context = CGContext(
|
||||
data: nil,
|
||||
width: Int(scaledSize.width),
|
||||
height: Int(scaledSize.height),
|
||||
bitsPerComponent: 8,
|
||||
bytesPerRow: 0,
|
||||
space: colorSpace,
|
||||
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
|
||||
) else { return nil }
|
||||
|
||||
// Clear to transparent
|
||||
context.clear(CGRect(origin: .zero, size: scaledSize))
|
||||
|
||||
// Get the PDF page bounds
|
||||
let pdfBox = page.getBoxRect(.bleedBox)
|
||||
|
||||
// Calculate scale to fit
|
||||
let xScale = scaledSize.width / pdfBox.width
|
||||
let yScale = scaledSize.height / pdfBox.height
|
||||
let fitScale = min(xScale, yScale)
|
||||
|
||||
// Apply transformations
|
||||
context.scaleBy(x: fitScale, y: fitScale)
|
||||
context.drawPDFPage(page)
|
||||
|
||||
guard let image = context.makeImage() else { return nil }
|
||||
|
||||
lock.lock()
|
||||
imageCache[cacheKey] = image
|
||||
lock.unlock()
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
/// Clears the image cache to free memory.
|
||||
public func clearCache() {
|
||||
lock.lock()
|
||||
imageCache.removeAll()
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,12 +2,15 @@
|
||||
// CardView.swift
|
||||
// CasinoKit
|
||||
//
|
||||
// Beautiful playing card view with flip animation.
|
||||
// Beautiful playing card view with vector PDF rendering for faces
|
||||
// and custom themed back design with flip animation.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// A single playing card with elegant design and flip animation.
|
||||
/// Uses vector PDF rendering for card faces (crisp at any size).
|
||||
/// Uses custom themed design for card backs.
|
||||
public struct CardView: View {
|
||||
let card: Card
|
||||
let isFaceUp: Bool
|
||||
@ -60,23 +63,14 @@ public struct CardView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// The front face of a playing card showing rank and suit.
|
||||
/// The front face of a playing card using vector PDF rendering.
|
||||
public struct CardFrontView: View {
|
||||
let card: Card
|
||||
let width: CGFloat
|
||||
let height: CGFloat
|
||||
let theme: any CasinoTheme
|
||||
|
||||
// MARK: - Layout Constants
|
||||
|
||||
private let rankFontRatio: CGFloat = 0.19
|
||||
private let suitFontRatio: CGFloat = 0.15
|
||||
private let centerSuitFontRatio: CGFloat = 0.4
|
||||
private let contentPaddingRatio: CGFloat = 0.04
|
||||
|
||||
private var suitColor: Color {
|
||||
card.suit.isRed ? .red : .black
|
||||
}
|
||||
@State private var image: CGImage?
|
||||
|
||||
public init(card: Card, width: CGFloat, height: CGFloat, theme: any CasinoTheme = DefaultCasinoTheme()) {
|
||||
self.card = card
|
||||
@ -87,72 +81,47 @@ public struct CardFrontView: View {
|
||||
|
||||
public var body: some View {
|
||||
ZStack {
|
||||
// Card background with subtle gradient
|
||||
// White card background
|
||||
RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.small)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [.white, theme.cardFrontColor],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.fill(Color.white)
|
||||
|
||||
// Card border
|
||||
// Card face from PDF
|
||||
if let image = image {
|
||||
Image(decorative: image, scale: 2.0)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
}
|
||||
|
||||
// Subtle border
|
||||
RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.small)
|
||||
.strokeBorder(
|
||||
LinearGradient(
|
||||
colors: [Color(white: 0.8), Color(white: 0.6)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
),
|
||||
Color.gray.opacity(CasinoDesign.Opacity.light),
|
||||
lineWidth: CasinoDesign.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()
|
||||
}
|
||||
|
||||
// Center suit (large)
|
||||
Text(card.suit.rawValue)
|
||||
.font(.system(size: width * centerSuitFontRatio))
|
||||
.foregroundStyle(suitColor)
|
||||
|
||||
// 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)
|
||||
.clipShape(RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.small))
|
||||
.shadow(
|
||||
color: .black.opacity(CasinoDesign.Opacity.light),
|
||||
radius: CasinoDesign.Shadow.radiusSmall,
|
||||
x: CasinoDesign.Shadow.offsetMedium,
|
||||
y: CasinoDesign.Shadow.offsetMedium
|
||||
)
|
||||
.onAppear {
|
||||
renderImage()
|
||||
}
|
||||
.onChange(of: card.pdfIndex) { _, _ in
|
||||
renderImage()
|
||||
}
|
||||
}
|
||||
|
||||
private func renderImage() {
|
||||
guard width > 0 && height > 0 else { return }
|
||||
image = CardRenderer.shared.renderCard(card, size: CGSize(width: width, height: height))
|
||||
}
|
||||
}
|
||||
|
||||
/// The back of a playing card with elegant pattern.
|
||||
/// The back of a playing card with elegant themed pattern.
|
||||
public struct CardBackView: View {
|
||||
let width: CGFloat
|
||||
let height: CGFloat
|
||||
@ -164,7 +133,6 @@ public struct CardBackView: View {
|
||||
private let patternPaddingRatio: CGFloat = 0.12
|
||||
private let emblemGradientRatio: CGFloat = 0.15
|
||||
private let emblemSizeRatio: CGFloat = 0.3
|
||||
private let logoFontRatio: CGFloat = 0.18
|
||||
|
||||
public init(width: CGFloat, height: CGFloat, theme: any CasinoTheme = DefaultCasinoTheme()) {
|
||||
self.width = width
|
||||
@ -299,16 +267,61 @@ public struct CardPlaceholderView: View {
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview("Card Views") {
|
||||
ZStack {
|
||||
Color(red: 0.05, green: 0.35, blue: 0.15)
|
||||
.ignoresSafeArea()
|
||||
|
||||
HStack(spacing: CasinoDesign.Spacing.xLarge) {
|
||||
CardView(card: Card(suit: .hearts, rank: .ace), isFaceUp: true, cardWidth: 95)
|
||||
CardView(card: Card(suit: .spades, rank: .king), isFaceUp: true, cardWidth: 95)
|
||||
CardView(card: Card(suit: .diamonds, rank: .seven), isFaceUp: false, cardWidth: 95)
|
||||
VStack(spacing: CasinoDesign.Spacing.large) {
|
||||
// Row 1: Face cards
|
||||
HStack(spacing: CasinoDesign.Spacing.medium) {
|
||||
CardView(card: Card(suit: .hearts, rank: .ace), isFaceUp: true, cardWidth: 70)
|
||||
CardView(card: Card(suit: .spades, rank: .king), isFaceUp: true, cardWidth: 70)
|
||||
CardView(card: Card(suit: .diamonds, rank: .queen), isFaceUp: true, cardWidth: 70)
|
||||
CardView(card: Card(suit: .clubs, rank: .jack), isFaceUp: true, cardWidth: 70)
|
||||
}
|
||||
|
||||
// Row 2: Number cards
|
||||
HStack(spacing: CasinoDesign.Spacing.medium) {
|
||||
CardView(card: Card(suit: .hearts, rank: .ten), isFaceUp: true, cardWidth: 70)
|
||||
CardView(card: Card(suit: .spades, rank: .seven), isFaceUp: true, cardWidth: 70)
|
||||
CardView(card: Card(suit: .diamonds, rank: .four), isFaceUp: true, cardWidth: 70)
|
||||
CardView(card: Card(suit: .clubs, rank: .two), isFaceUp: true, cardWidth: 70)
|
||||
}
|
||||
|
||||
// Row 3: Card back (custom themed design)
|
||||
HStack(spacing: CasinoDesign.Spacing.medium) {
|
||||
CardView(card: Card(suit: .hearts, rank: .ace), isFaceUp: false, cardWidth: 70)
|
||||
CardView(card: Card(suit: .spades, rank: .ace), isFaceUp: false, cardWidth: 70)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("All Suits - Aces") {
|
||||
ZStack {
|
||||
Color(red: 0.05, green: 0.35, blue: 0.15)
|
||||
.ignoresSafeArea()
|
||||
|
||||
HStack(spacing: CasinoDesign.Spacing.medium) {
|
||||
CardView(card: Card(suit: .spades, rank: .ace), cardWidth: 80)
|
||||
CardView(card: Card(suit: .hearts, rank: .ace), cardWidth: 80)
|
||||
CardView(card: Card(suit: .clubs, rank: .ace), cardWidth: 80)
|
||||
CardView(card: Card(suit: .diamonds, rank: .ace), cardWidth: 80)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Large Cards") {
|
||||
ZStack {
|
||||
Color(red: 0.05, green: 0.35, blue: 0.15)
|
||||
.ignoresSafeArea()
|
||||
|
||||
HStack(spacing: CasinoDesign.Spacing.large) {
|
||||
CardView(card: Card(suit: .hearts, rank: .king), cardWidth: 120)
|
||||
CardView(card: Card(suit: .spades, rank: .ace), isFaceUp: false, cardWidth: 120)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user