Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2025-12-16 20:30:26 -06:00
parent c846ef05ac
commit b08e92a402
7 changed files with 108 additions and 53 deletions

View File

@ -43,7 +43,9 @@ enum Design {
static let xSmall: CGFloat = 9
static let small: CGFloat = 10
static let body: CGFloat = 12
static let callout: CGFloat = 13
static let medium: CGFloat = 14
static let subheadline: CGFloat = 15
static let large: CGFloat = 16
static let xLarge: CGFloat = 18
static let xxLarge: CGFloat = 20
@ -83,22 +85,46 @@ enum Design {
static let quick: Double = 0.3
static let springDuration: Double = 0.4
static let springBounce: Double = 0.3
static let cardFlipBounce: Double = 0.2
static let fadeInDuration: Double = 0.3
static let cardFlipDuration: Double = 0.5
static let selectionDuration: Double = 0.2
static let staggerDelay1: Double = 0.2
static let staggerDelay2: Double = 0.4
}
// MARK: - Opacity
enum Opacity {
static let disabled: Double = 0.5
static let verySubtle: Double = 0.05
static let subtle: Double = 0.1
static let hint: Double = 0.2
static let light: Double = 0.3
static let overlay: Double = 0.4
static let medium: Double = 0.5
static let secondary: Double = 0.5
static let disabled: Double = 0.5
static let strong: Double = 0.7
static let heavy: Double = 0.8
static let nearOpaque: Double = 0.85
static let almostFull: Double = 0.9
}
// MARK: - Scale Effects
enum Scale {
static let shrunk: Double = 0.5
static let slightShrink: Double = 0.8
static let normal: Double = 1.0
static let selected: Double = 1.1
}
// MARK: - Minimum Scale Factor (for text)
enum MinScaleFactor {
static let tight: Double = 0.5
static let comfortable: Double = 0.6
static let relaxed: Double = 0.7
}
// MARK: - Line Widths

View File

@ -35,7 +35,7 @@ struct CardView: View {
.degrees(isFaceUp ? 0 : 180),
axis: (x: 0, y: 1, z: 0)
)
.animation(.spring(duration: 0.4, bounce: 0.2), value: isFaceUp)
.animation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.cardFlipBounce), value: isFaceUp)
}
}
@ -45,6 +45,16 @@ struct CardFrontView: View {
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
}
@ -52,24 +62,24 @@ struct CardFrontView: View {
var body: some View {
ZStack {
// Card background with subtle gradient
RoundedRectangle(cornerRadius: 8)
RoundedRectangle(cornerRadius: Design.CornerRadius.small)
.fill(
LinearGradient(
colors: [.white, Color(white: 0.96)],
colors: [.white, Color(white: backgroundWhite)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
// Card border
RoundedRectangle(cornerRadius: 8)
RoundedRectangle(cornerRadius: Design.CornerRadius.small)
.strokeBorder(
LinearGradient(
colors: [Color(white: 0.8), Color(white: 0.6)],
colors: [Color(white: borderLightGray), Color(white: borderDarkGray)],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: 1
lineWidth: Design.LineWidth.thin
)
// Card content
@ -78,9 +88,9 @@ struct CardFrontView: View {
HStack {
VStack(spacing: 0) {
Text(card.rank.symbol)
.font(.system(size: width * 0.22, weight: .bold, design: .serif))
.font(.system(size: width * rankFontRatio, weight: .bold, design: .serif))
Text(card.suit.rawValue)
.font(.system(size: width * 0.18))
.font(.system(size: width * suitFontRatio))
}
.foregroundStyle(suitColor)
Spacer()
@ -90,7 +100,7 @@ struct CardFrontView: View {
// Center suit (large)
Text(card.suit.rawValue)
.font(.system(size: width * 0.5))
.font(.system(size: width * centerSuitFontRatio))
.foregroundStyle(suitColor)
Spacer()
@ -100,18 +110,18 @@ struct CardFrontView: View {
Spacer()
VStack(spacing: 0) {
Text(card.suit.rawValue)
.font(.system(size: width * 0.18))
.font(.system(size: width * suitFontRatio))
Text(card.rank.symbol)
.font(.system(size: width * 0.22, weight: .bold, design: .serif))
.font(.system(size: width * rankFontRatio, weight: .bold, design: .serif))
}
.foregroundStyle(suitColor)
.rotationEffect(.degrees(180))
}
}
.padding(width * 0.08)
.padding(width * contentPaddingRatio)
}
.frame(width: width, height: height)
.shadow(color: .black.opacity(0.2), radius: 4, x: 2, y: 2)
.shadow(color: .black.opacity(Design.Opacity.hint), radius: Design.Shadow.radiusSmall, x: 2, y: 2)
}
}
@ -120,6 +130,14 @@ 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
@ -161,14 +179,14 @@ struct CardBackView: View {
endPoint: .bottom
)
)
.padding(width * 0.1)
.padding(width * innerPaddingRatio)
// Diamond pattern overlay
DiamondPatternView()
.foregroundStyle(
Color.Card.diamondPattern.opacity(Design.Opacity.light)
)
.padding(width * 0.12)
.padding(width * patternPaddingRatio)
.clipShape(RoundedRectangle(cornerRadius: Design.CornerRadius.small / 2))
// Center emblem
@ -181,14 +199,14 @@ struct CardBackView: View {
],
center: .center,
startRadius: 0,
endRadius: width * 0.15
endRadius: width * emblemGradientRatio
)
)
.frame(width: width * 0.3, height: width * 0.3)
.frame(width: width * emblemSizeRatio, height: width * emblemSizeRatio)
// B for Baccarat
Text("B")
.font(.system(size: width * 0.18, weight: .bold, design: .serif))
.font(.system(size: width * logoFontRatio, weight: .bold, design: .serif))
.foregroundStyle(Color.Card.logoText)
}
.frame(width: width, height: height)

View File

@ -19,6 +19,15 @@ struct ChipView: View {
self.isSelected = isSelected
}
// MARK: - Layout Constants
private let innerCircleRatio: CGFloat = 0.65
private let innerGradientRatio: CGFloat = 0.4
private let textSizeRatio: CGFloat = 0.25
private let selectionGlowPadding: CGFloat = 6
private let shadowOffset: CGFloat = 2
private let shadowOffsetY: CGFloat = 3
var body: some View {
ZStack {
// Base circle with gradient
@ -28,7 +37,7 @@ struct ChipView: View {
colors: [
denomination.secondaryColor,
denomination.primaryColor,
denomination.primaryColor.opacity(0.8)
denomination.primaryColor.opacity(Design.Opacity.heavy)
],
center: .topLeading,
startRadius: 0,
@ -50,24 +59,24 @@ struct ChipView: View {
],
center: .topLeading,
startRadius: 0,
endRadius: size * 0.4
endRadius: size * innerGradientRatio
)
)
.frame(width: size * 0.65, height: size * 0.65)
.frame(width: size * innerCircleRatio, height: size * innerCircleRatio)
// Inner border
Circle()
.strokeBorder(
denomination.stripeColor.opacity(0.8),
denomination.stripeColor.opacity(Design.Opacity.heavy),
lineWidth: Design.LineWidth.medium
)
.frame(width: size * 0.65, height: size * 0.65)
.frame(width: size * innerCircleRatio, height: size * innerCircleRatio)
// Denomination text
Text(denomination.displayText)
.font(.system(size: size * 0.25, weight: .heavy, design: .rounded))
.font(.system(size: size * textSizeRatio, weight: .heavy, design: .rounded))
.foregroundStyle(denomination.stripeColor)
.shadow(color: .black.opacity(Design.Opacity.light), radius: 1, x: 1, y: 1)
.shadow(color: .black.opacity(Design.Opacity.light), radius: Design.LineWidth.thin, x: 1, y: 1)
// Outer border
Circle()
@ -87,13 +96,13 @@ struct ChipView: View {
if isSelected {
Circle()
.strokeBorder(Color.yellow, lineWidth: Design.LineWidth.thick)
.frame(width: size + 6, height: size + 6)
.frame(width: size + selectionGlowPadding, height: size + selectionGlowPadding)
}
}
.frame(width: size, height: size)
.shadow(color: .black.opacity(Design.Opacity.overlay), radius: isSelected ? 8 : 4, x: 2, y: 3)
.scaleEffect(isSelected ? 1.1 : 1.0)
.animation(.spring(duration: 0.2), value: isSelected)
.shadow(color: .black.opacity(Design.Opacity.overlay), radius: isSelected ? Design.Shadow.radiusSmall * 2 : Design.Shadow.radiusSmall, x: shadowOffset, y: shadowOffsetY)
.scaleEffect(isSelected ? Design.Scale.selected : Design.Scale.normal)
.animation(.spring(duration: Design.Animation.selectionDuration), value: isSelected)
}
}

View File

@ -267,7 +267,7 @@ struct GameOverView: View {
)
.shadow(color: .red.opacity(0.2), radius: Design.Shadow.radiusXXLarge)
.padding(.horizontal, Design.Spacing.xxLarge)
.scaleEffect(showContent ? 1.0 : 0.8)
.scaleEffect(showContent ? Design.Scale.normal : Design.Scale.slightShrink)
.opacity(showContent ? 1.0 : 0)
}
.onAppear {
@ -366,11 +366,13 @@ struct CompactHandView: View {
// Fixed size: cards have strict visual constraints
private let cardWidth: CGFloat = 45
private let cardOverlap: CGFloat = -12
private let placeholderSpacing: CGFloat = 8
var body: some View {
HStack(spacing: -12) {
HStack(spacing: cards.isEmpty ? placeholderSpacing : cardOverlap) {
if cards.isEmpty {
// Placeholders
// Placeholders - no overlap, just side by side
ForEach(0..<2, id: \.self) { _ in
CardPlaceholderView(width: cardWidth)
}
@ -516,7 +518,7 @@ struct TopBarView: View {
.contentTransition(.numericText())
.animation(.spring(duration: Design.Animation.quick), value: balance)
.lineLimit(1)
.minimumScaleFactor(0.5)
.minimumScaleFactor(Design.MinScaleFactor.tight)
}
.padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.xSmall)
@ -611,7 +613,7 @@ struct ActionButtonsView: View {
.font(.system(size: statusFontSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.heavy))
.lineLimit(1)
.minimumScaleFactor(0.7)
.minimumScaleFactor(Design.MinScaleFactor.relaxed)
}
.padding(.horizontal, Design.Spacing.xLarge)
.padding(.vertical, Design.Spacing.medium)

View File

@ -70,7 +70,7 @@ struct MiniBaccaratTableView: View {
.foregroundStyle(.white.opacity(Design.Opacity.medium))
.tracking(1)
.lineLimit(1)
.minimumScaleFactor(0.6)
.minimumScaleFactor(Design.MinScaleFactor.comfortable)
ZStack {
// Table felt background with arc shape
@ -248,13 +248,13 @@ struct TieBettingZone: View {
.font(.system(size: titleFontSize, weight: .black, design: .rounded))
.tracking(2)
.lineLimit(1)
.minimumScaleFactor(0.5)
.minimumScaleFactor(Design.MinScaleFactor.tight)
Text("PAYS 8 TO 1")
.font(.system(size: subtitleFontSize, weight: .medium))
.opacity(Design.Opacity.heavy)
.lineLimit(1)
.minimumScaleFactor(0.5)
.minimumScaleFactor(Design.MinScaleFactor.tight)
}
.foregroundStyle(.white)
}
@ -338,13 +338,13 @@ struct BankerBettingZone: View {
.font(.system(size: titleFontSize, weight: .black, design: .rounded))
.tracking(3)
.lineLimit(1)
.minimumScaleFactor(0.5)
.minimumScaleFactor(Design.MinScaleFactor.tight)
Text("PAYS 0.95 TO 1")
.font(.system(size: subtitleFontSize, weight: .medium))
.opacity(Design.Opacity.heavy)
.lineLimit(1)
.minimumScaleFactor(0.5)
.minimumScaleFactor(Design.MinScaleFactor.tight)
}
.foregroundStyle(.white)
}
@ -428,13 +428,13 @@ struct PlayerBettingZone: View {
.font(.system(size: titleFontSize, weight: .black, design: .rounded))
.tracking(3)
.lineLimit(1)
.minimumScaleFactor(0.5)
.minimumScaleFactor(Design.MinScaleFactor.tight)
Text("PAYS 1 TO 1")
.font(.system(size: subtitleFontSize, weight: .medium))
.opacity(Design.Opacity.heavy)
.lineLimit(1)
.minimumScaleFactor(0.5)
.minimumScaleFactor(Design.MinScaleFactor.tight)
}
.foregroundStyle(.white)
}

View File

@ -41,8 +41,8 @@ struct ResultBannerView: View {
)
)
.shadow(color: result.color.opacity(Design.Opacity.heavy), radius: Design.Shadow.radiusLarge)
.scaleEffect(showText ? 1.0 : 0.5)
.opacity(showText ? 1.0 : 0)
.scaleEffect(showText ? Design.Scale.normal : Design.Scale.shrunk)
.opacity(showText ? Design.Scale.normal : 0)
// Winnings display
if winnings != 0 {
@ -60,8 +60,8 @@ struct ResultBannerView: View {
}
}
.font(.system(size: winningsFontSize, weight: .bold, design: .rounded))
.scaleEffect(showWinnings ? 1.0 : 0.5)
.opacity(showWinnings ? 1.0 : 0)
.scaleEffect(showWinnings ? Design.Scale.normal : Design.Scale.shrunk)
.opacity(showWinnings ? Design.Scale.normal : 0)
}
}
.padding(Design.Spacing.xxxLarge + Design.Spacing.small)
@ -93,19 +93,19 @@ struct ResultBannerView: View {
)
)
.shadow(color: result.color.opacity(Design.Opacity.light), radius: Design.Shadow.radiusXXLarge)
.scaleEffect(showBanner ? 1.0 : 0.8)
.opacity(showBanner ? 1.0 : 0)
.scaleEffect(showBanner ? Design.Scale.normal : Design.Scale.slightShrink)
.opacity(showBanner ? Design.Scale.normal : 0)
}
.onAppear {
withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce)) {
showBanner = true
}
withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce).delay(0.2)) {
withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce).delay(Design.Animation.staggerDelay1)) {
showText = true
}
withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce).delay(0.4)) {
withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce).delay(Design.Animation.staggerDelay2)) {
showWinnings = true
}
}

View File

@ -265,7 +265,7 @@ struct SettingsToggle: View {
Toggle(isOn: $isOn) {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(title)
.font(.system(size: 15, weight: .medium))
.font(.system(size: Design.BaseFontSize.subheadline, weight: .medium))
.foregroundStyle(.white)
Text(subtitle)
@ -290,7 +290,7 @@ struct SpeedPicker: View {
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text("Dealing Speed")
.font(.system(size: 15, weight: .medium))
.font(.system(size: Design.BaseFontSize.subheadline, weight: .medium))
.foregroundStyle(.white)
HStack(spacing: Design.Spacing.small) {
@ -299,7 +299,7 @@ struct SpeedPicker: View {
speed = option.1
} label: {
Text(option.0)
.font(.system(size: 13, weight: .medium))
.font(.system(size: Design.BaseFontSize.callout, weight: .medium))
.foregroundStyle(speed == option.1 ? .black : .white.opacity(Design.Opacity.strong))
.padding(.vertical, Design.Spacing.small)
.frame(maxWidth: .infinity)