diff --git a/Baccarat/Theme/DesignConstants.swift b/Baccarat/Theme/DesignConstants.swift index d3482e4..d6f9dc8 100644 --- a/Baccarat/Theme/DesignConstants.swift +++ b/Baccarat/Theme/DesignConstants.swift @@ -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 diff --git a/Baccarat/Views/CardView.swift b/Baccarat/Views/CardView.swift index cbe26c8..cd3ab7f 100644 --- a/Baccarat/Views/CardView.swift +++ b/Baccarat/Views/CardView.swift @@ -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) diff --git a/Baccarat/Views/Chips/ChipView.swift b/Baccarat/Views/Chips/ChipView.swift index 9b41f0a..f8a1555 100644 --- a/Baccarat/Views/Chips/ChipView.swift +++ b/Baccarat/Views/Chips/ChipView.swift @@ -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) } } diff --git a/Baccarat/Views/GameTableView.swift b/Baccarat/Views/GameTableView.swift index a56e882..1f0bb0a 100644 --- a/Baccarat/Views/GameTableView.swift +++ b/Baccarat/Views/GameTableView.swift @@ -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) diff --git a/Baccarat/Views/MiniBaccaratTableView.swift b/Baccarat/Views/MiniBaccaratTableView.swift index e0ff9af..b61152e 100644 --- a/Baccarat/Views/MiniBaccaratTableView.swift +++ b/Baccarat/Views/MiniBaccaratTableView.swift @@ -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) } diff --git a/Baccarat/Views/ResultBannerView.swift b/Baccarat/Views/ResultBannerView.swift index 0afe6e3..43b80bb 100644 --- a/Baccarat/Views/ResultBannerView.swift +++ b/Baccarat/Views/ResultBannerView.swift @@ -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 } } diff --git a/Baccarat/Views/SettingsView.swift b/Baccarat/Views/SettingsView.swift index e505cdd..faf2a9a 100644 --- a/Baccarat/Views/SettingsView.swift +++ b/Baccarat/Views/SettingsView.swift @@ -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)