Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
03ee03b876
commit
fa5d9f4c75
@ -45,6 +45,8 @@
|
|||||||
// - BettingZone
|
// - BettingZone
|
||||||
|
|
||||||
// MARK: - Cards (additional)
|
// MARK: - Cards (additional)
|
||||||
|
// - CardRenderer (PDF-based vector rendering for card faces)
|
||||||
|
// - CardHand, CardFan, CardsGrid (layout views)
|
||||||
// - HandDisplayView
|
// - HandDisplayView
|
||||||
|
|
||||||
// MARK: - Settings
|
// MARK: - Settings
|
||||||
|
|||||||
@ -142,5 +142,25 @@ public struct Card: Identifiable, Equatable, Sendable {
|
|||||||
public var accessibilityDescription: String {
|
public var accessibilityDescription: String {
|
||||||
"\(rank.accessibilityName) of \(suit.accessibilityName)"
|
"\(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
|
// CardView.swift
|
||||||
// CasinoKit
|
// 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
|
import SwiftUI
|
||||||
|
|
||||||
/// A single playing card with elegant design and flip animation.
|
/// 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 {
|
public struct CardView: View {
|
||||||
let card: Card
|
let card: Card
|
||||||
let isFaceUp: Bool
|
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 {
|
public struct CardFrontView: View {
|
||||||
let card: Card
|
let card: Card
|
||||||
let width: CGFloat
|
let width: CGFloat
|
||||||
let height: CGFloat
|
let height: CGFloat
|
||||||
let theme: any CasinoTheme
|
let theme: any CasinoTheme
|
||||||
|
|
||||||
// MARK: - Layout Constants
|
@State private var image: CGImage?
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(card: Card, width: CGFloat, height: CGFloat, theme: any CasinoTheme = DefaultCasinoTheme()) {
|
public init(card: Card, width: CGFloat, height: CGFloat, theme: any CasinoTheme = DefaultCasinoTheme()) {
|
||||||
self.card = card
|
self.card = card
|
||||||
@ -87,72 +81,47 @@ public struct CardFrontView: View {
|
|||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
// Card background with subtle gradient
|
// White card background
|
||||||
RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.small)
|
RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.small)
|
||||||
.fill(
|
.fill(Color.white)
|
||||||
LinearGradient(
|
|
||||||
colors: [.white, theme.cardFrontColor],
|
|
||||||
startPoint: .topLeading,
|
|
||||||
endPoint: .bottomTrailing
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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)
|
RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.small)
|
||||||
.strokeBorder(
|
.strokeBorder(
|
||||||
LinearGradient(
|
Color.gray.opacity(CasinoDesign.Opacity.light),
|
||||||
colors: [Color(white: 0.8), Color(white: 0.6)],
|
|
||||||
startPoint: .topLeading,
|
|
||||||
endPoint: .bottomTrailing
|
|
||||||
),
|
|
||||||
lineWidth: CasinoDesign.LineWidth.thin
|
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)
|
.frame(width: width, height: height)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.small))
|
||||||
.shadow(
|
.shadow(
|
||||||
color: .black.opacity(CasinoDesign.Opacity.light),
|
color: .black.opacity(CasinoDesign.Opacity.light),
|
||||||
radius: CasinoDesign.Shadow.radiusSmall,
|
radius: CasinoDesign.Shadow.radiusSmall,
|
||||||
x: CasinoDesign.Shadow.offsetMedium,
|
x: CasinoDesign.Shadow.offsetMedium,
|
||||||
y: CasinoDesign.Shadow.offsetMedium
|
y: CasinoDesign.Shadow.offsetMedium
|
||||||
)
|
)
|
||||||
|
.onAppear {
|
||||||
|
renderImage()
|
||||||
|
}
|
||||||
|
.onChange(of: card.pdfIndex) { _, _ in
|
||||||
|
renderImage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The back of a playing card with elegant pattern.
|
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 themed pattern.
|
||||||
public struct CardBackView: View {
|
public struct CardBackView: View {
|
||||||
let width: CGFloat
|
let width: CGFloat
|
||||||
let height: CGFloat
|
let height: CGFloat
|
||||||
@ -164,7 +133,6 @@ public struct CardBackView: View {
|
|||||||
private let patternPaddingRatio: CGFloat = 0.12
|
private let patternPaddingRatio: CGFloat = 0.12
|
||||||
private let emblemGradientRatio: CGFloat = 0.15
|
private let emblemGradientRatio: CGFloat = 0.15
|
||||||
private let emblemSizeRatio: CGFloat = 0.3
|
private let emblemSizeRatio: CGFloat = 0.3
|
||||||
private let logoFontRatio: CGFloat = 0.18
|
|
||||||
|
|
||||||
public init(width: CGFloat, height: CGFloat, theme: any CasinoTheme = DefaultCasinoTheme()) {
|
public init(width: CGFloat, height: CGFloat, theme: any CasinoTheme = DefaultCasinoTheme()) {
|
||||||
self.width = width
|
self.width = width
|
||||||
@ -299,16 +267,61 @@ public struct CardPlaceholderView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
// MARK: - Previews
|
||||||
|
|
||||||
|
#Preview("Card Views") {
|
||||||
ZStack {
|
ZStack {
|
||||||
Color(red: 0.05, green: 0.35, blue: 0.15)
|
Color(red: 0.05, green: 0.35, blue: 0.15)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
|
||||||
HStack(spacing: CasinoDesign.Spacing.xLarge) {
|
VStack(spacing: CasinoDesign.Spacing.large) {
|
||||||
CardView(card: Card(suit: .hearts, rank: .ace), isFaceUp: true, cardWidth: 95)
|
// Row 1: Face cards
|
||||||
CardView(card: Card(suit: .spades, rank: .king), isFaceUp: true, cardWidth: 95)
|
HStack(spacing: CasinoDesign.Spacing.medium) {
|
||||||
CardView(card: Card(suit: .diamonds, rank: .seven), isFaceUp: false, cardWidth: 95)
|
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