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 xSmall: CGFloat = 9
static let small: CGFloat = 10 static let small: CGFloat = 10
static let body: CGFloat = 12 static let body: CGFloat = 12
static let callout: CGFloat = 13
static let medium: CGFloat = 14 static let medium: CGFloat = 14
static let subheadline: CGFloat = 15
static let large: CGFloat = 16 static let large: CGFloat = 16
static let xLarge: CGFloat = 18 static let xLarge: CGFloat = 18
static let xxLarge: CGFloat = 20 static let xxLarge: CGFloat = 20
@ -83,22 +85,46 @@ enum Design {
static let quick: Double = 0.3 static let quick: Double = 0.3
static let springDuration: Double = 0.4 static let springDuration: Double = 0.4
static let springBounce: Double = 0.3 static let springBounce: Double = 0.3
static let cardFlipBounce: Double = 0.2
static let fadeInDuration: Double = 0.3 static let fadeInDuration: Double = 0.3
static let cardFlipDuration: Double = 0.5 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 // MARK: - Opacity
enum Opacity { enum Opacity {
static let disabled: Double = 0.5 static let verySubtle: Double = 0.05
static let subtle: Double = 0.1 static let subtle: Double = 0.1
static let hint: Double = 0.2
static let light: Double = 0.3 static let light: Double = 0.3
static let overlay: Double = 0.4 static let overlay: Double = 0.4
static let medium: Double = 0.5 static let medium: Double = 0.5
static let secondary: Double = 0.5 static let secondary: Double = 0.5
static let disabled: Double = 0.5
static let strong: Double = 0.7 static let strong: Double = 0.7
static let heavy: Double = 0.8 static let heavy: Double = 0.8
static let nearOpaque: Double = 0.85 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 // MARK: - Line Widths

View File

@ -35,7 +35,7 @@ struct CardView: View {
.degrees(isFaceUp ? 0 : 180), .degrees(isFaceUp ? 0 : 180),
axis: (x: 0, y: 1, z: 0) 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 width: CGFloat
let height: 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 { private var suitColor: Color {
card.suit.isRed ? .red : .black card.suit.isRed ? .red : .black
} }
@ -52,24 +62,24 @@ struct CardFrontView: View {
var body: some View { var body: some View {
ZStack { ZStack {
// Card background with subtle gradient // Card background with subtle gradient
RoundedRectangle(cornerRadius: 8) RoundedRectangle(cornerRadius: Design.CornerRadius.small)
.fill( .fill(
LinearGradient( LinearGradient(
colors: [.white, Color(white: 0.96)], colors: [.white, Color(white: backgroundWhite)],
startPoint: .topLeading, startPoint: .topLeading,
endPoint: .bottomTrailing endPoint: .bottomTrailing
) )
) )
// Card border // Card border
RoundedRectangle(cornerRadius: 8) RoundedRectangle(cornerRadius: Design.CornerRadius.small)
.strokeBorder( .strokeBorder(
LinearGradient( LinearGradient(
colors: [Color(white: 0.8), Color(white: 0.6)], colors: [Color(white: borderLightGray), Color(white: borderDarkGray)],
startPoint: .topLeading, startPoint: .topLeading,
endPoint: .bottomTrailing endPoint: .bottomTrailing
), ),
lineWidth: 1 lineWidth: Design.LineWidth.thin
) )
// Card content // Card content
@ -78,9 +88,9 @@ struct CardFrontView: View {
HStack { HStack {
VStack(spacing: 0) { VStack(spacing: 0) {
Text(card.rank.symbol) 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) Text(card.suit.rawValue)
.font(.system(size: width * 0.18)) .font(.system(size: width * suitFontRatio))
} }
.foregroundStyle(suitColor) .foregroundStyle(suitColor)
Spacer() Spacer()
@ -90,7 +100,7 @@ struct CardFrontView: View {
// Center suit (large) // Center suit (large)
Text(card.suit.rawValue) Text(card.suit.rawValue)
.font(.system(size: width * 0.5)) .font(.system(size: width * centerSuitFontRatio))
.foregroundStyle(suitColor) .foregroundStyle(suitColor)
Spacer() Spacer()
@ -100,18 +110,18 @@ struct CardFrontView: View {
Spacer() Spacer()
VStack(spacing: 0) { VStack(spacing: 0) {
Text(card.suit.rawValue) Text(card.suit.rawValue)
.font(.system(size: width * 0.18)) .font(.system(size: width * suitFontRatio))
Text(card.rank.symbol) 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) .foregroundStyle(suitColor)
.rotationEffect(.degrees(180)) .rotationEffect(.degrees(180))
} }
} }
.padding(width * 0.08) .padding(width * contentPaddingRatio)
} }
.frame(width: width, height: height) .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 width: CGFloat
let height: 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 { var body: some View {
ZStack { ZStack {
// Base // Base
@ -161,14 +179,14 @@ struct CardBackView: View {
endPoint: .bottom endPoint: .bottom
) )
) )
.padding(width * 0.1) .padding(width * innerPaddingRatio)
// Diamond pattern overlay // Diamond pattern overlay
DiamondPatternView() DiamondPatternView()
.foregroundStyle( .foregroundStyle(
Color.Card.diamondPattern.opacity(Design.Opacity.light) Color.Card.diamondPattern.opacity(Design.Opacity.light)
) )
.padding(width * 0.12) .padding(width * patternPaddingRatio)
.clipShape(RoundedRectangle(cornerRadius: Design.CornerRadius.small / 2)) .clipShape(RoundedRectangle(cornerRadius: Design.CornerRadius.small / 2))
// Center emblem // Center emblem
@ -181,14 +199,14 @@ struct CardBackView: View {
], ],
center: .center, center: .center,
startRadius: 0, 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 // B for Baccarat
Text("B") 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) .foregroundStyle(Color.Card.logoText)
} }
.frame(width: width, height: height) .frame(width: width, height: height)

View File

@ -19,6 +19,15 @@ struct ChipView: View {
self.isSelected = isSelected 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 { var body: some View {
ZStack { ZStack {
// Base circle with gradient // Base circle with gradient
@ -28,7 +37,7 @@ struct ChipView: View {
colors: [ colors: [
denomination.secondaryColor, denomination.secondaryColor,
denomination.primaryColor, denomination.primaryColor,
denomination.primaryColor.opacity(0.8) denomination.primaryColor.opacity(Design.Opacity.heavy)
], ],
center: .topLeading, center: .topLeading,
startRadius: 0, startRadius: 0,
@ -50,24 +59,24 @@ struct ChipView: View {
], ],
center: .topLeading, center: .topLeading,
startRadius: 0, 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 // Inner border
Circle() Circle()
.strokeBorder( .strokeBorder(
denomination.stripeColor.opacity(0.8), denomination.stripeColor.opacity(Design.Opacity.heavy),
lineWidth: Design.LineWidth.medium lineWidth: Design.LineWidth.medium
) )
.frame(width: size * 0.65, height: size * 0.65) .frame(width: size * innerCircleRatio, height: size * innerCircleRatio)
// Denomination text // Denomination text
Text(denomination.displayText) 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) .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 // Outer border
Circle() Circle()
@ -87,13 +96,13 @@ struct ChipView: View {
if isSelected { if isSelected {
Circle() Circle()
.strokeBorder(Color.yellow, lineWidth: Design.LineWidth.thick) .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) .frame(width: size, height: size)
.shadow(color: .black.opacity(Design.Opacity.overlay), radius: isSelected ? 8 : 4, x: 2, y: 3) .shadow(color: .black.opacity(Design.Opacity.overlay), radius: isSelected ? Design.Shadow.radiusSmall * 2 : Design.Shadow.radiusSmall, x: shadowOffset, y: shadowOffsetY)
.scaleEffect(isSelected ? 1.1 : 1.0) .scaleEffect(isSelected ? Design.Scale.selected : Design.Scale.normal)
.animation(.spring(duration: 0.2), value: isSelected) .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) .shadow(color: .red.opacity(0.2), radius: Design.Shadow.radiusXXLarge)
.padding(.horizontal, Design.Spacing.xxLarge) .padding(.horizontal, Design.Spacing.xxLarge)
.scaleEffect(showContent ? 1.0 : 0.8) .scaleEffect(showContent ? Design.Scale.normal : Design.Scale.slightShrink)
.opacity(showContent ? 1.0 : 0) .opacity(showContent ? 1.0 : 0)
} }
.onAppear { .onAppear {
@ -366,11 +366,13 @@ struct CompactHandView: View {
// Fixed size: cards have strict visual constraints // Fixed size: cards have strict visual constraints
private let cardWidth: CGFloat = 45 private let cardWidth: CGFloat = 45
private let cardOverlap: CGFloat = -12
private let placeholderSpacing: CGFloat = 8
var body: some View { var body: some View {
HStack(spacing: -12) { HStack(spacing: cards.isEmpty ? placeholderSpacing : cardOverlap) {
if cards.isEmpty { if cards.isEmpty {
// Placeholders // Placeholders - no overlap, just side by side
ForEach(0..<2, id: \.self) { _ in ForEach(0..<2, id: \.self) { _ in
CardPlaceholderView(width: cardWidth) CardPlaceholderView(width: cardWidth)
} }
@ -516,7 +518,7 @@ struct TopBarView: View {
.contentTransition(.numericText()) .contentTransition(.numericText())
.animation(.spring(duration: Design.Animation.quick), value: balance) .animation(.spring(duration: Design.Animation.quick), value: balance)
.lineLimit(1) .lineLimit(1)
.minimumScaleFactor(0.5) .minimumScaleFactor(Design.MinScaleFactor.tight)
} }
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.xSmall) .padding(.vertical, Design.Spacing.xSmall)
@ -611,7 +613,7 @@ struct ActionButtonsView: View {
.font(.system(size: statusFontSize, weight: .medium)) .font(.system(size: statusFontSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.heavy)) .foregroundStyle(.white.opacity(Design.Opacity.heavy))
.lineLimit(1) .lineLimit(1)
.minimumScaleFactor(0.7) .minimumScaleFactor(Design.MinScaleFactor.relaxed)
} }
.padding(.horizontal, Design.Spacing.xLarge) .padding(.horizontal, Design.Spacing.xLarge)
.padding(.vertical, Design.Spacing.medium) .padding(.vertical, Design.Spacing.medium)

View File

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

View File

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

View File

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