From 109b2416fc47dc26857e70ed74e636c737a72e76 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Sat, 24 Jan 2026 16:29:44 -0600 Subject: [PATCH] Signed-off-by: Matt Bruce --- .../Views/Table/InteractiveCardView.swift | 205 +++++++++++++----- .../Baccarat/Views/Table/PageCurlView.swift | 15 +- 2 files changed, 161 insertions(+), 59 deletions(-) diff --git a/Baccarat/Baccarat/Views/Table/InteractiveCardView.swift b/Baccarat/Baccarat/Views/Table/InteractiveCardView.swift index ecaf97c..9471e13 100644 --- a/Baccarat/Baccarat/Views/Table/InteractiveCardView.swift +++ b/Baccarat/Baccarat/Views/Table/InteractiveCardView.swift @@ -20,33 +20,36 @@ struct InteractiveCardView: View { let onReveal: () -> Void let onUpdateProgress: (Double) -> Void + /// Whether user is currently interacting with the card (peeling) + @State private var isInteracting = false + /// Whether to show the glowing animation around this card + /// Only show when waiting, face down, bottom hand, not auto mode, AND not currently being touched private var showGlow: Bool { - isWaiting && !isFaceUp && isBottomHand && revealStyle != .auto + isWaiting && !isFaceUp && isBottomHand && revealStyle != .auto && !isInteracting } /// Animation state for the pulsing glow @State private var glowPulse = false + /// Tracks if we just completed a squeeze reveal (to skip flip animation) + @State private var squeezeJustCompleted = false + + private var cardHeight: CGFloat { + cardWidth * CasinoDesign.Size.cardAspectRatio + } + var body: some View { ZStack { - if revealStyle == .squeeze && !isFaceUp && isWaiting { - PageCurlView( - card: card, - width: cardWidth, - onReveal: onReveal - ) - .modifier(GlowModifier(isActive: showGlow, glowPulse: glowPulse, cardWidth: cardWidth)) - } else { - // Standard CardView (handles its own flip animation) - CardView(card: card, isFaceUp: isFaceUp, cardWidth: cardWidth) - .modifier(GlowModifier(isActive: showGlow, glowPulse: glowPulse, cardWidth: cardWidth)) - .onTapGesture { - if !isFaceUp && isWaiting && revealStyle == .tap { - onReveal() - } - } - } + // Glow layer - completely separate from the card, sits underneath + GlowBackgroundView( + isActive: showGlow, + glowPulse: glowPulse, + cardWidth: cardWidth + ) + + // Card layer - completely independent, no glow modifiers + cardContent } .onAppear { if showGlow { @@ -56,13 +59,100 @@ struct InteractiveCardView: View { .onChange(of: showGlow) { _, newValue in glowPulse = newValue } + .onChange(of: isWaiting) { _, newValue in + // Reset interaction state when this card is no longer waiting + if !newValue { + isInteracting = false + } + } + } + + /// The card content - completely isolated from glow state + @ViewBuilder + private var cardContent: some View { + if revealStyle == .squeeze && !isFaceUp && isWaiting { + PageCurlView( + card: card, + width: cardWidth, + onReveal: { + // Mark that squeeze just completed to skip flip animation + squeezeJustCompleted = true + onReveal() + // Reset after a short delay + DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { + squeezeJustCompleted = false + } + } + ) + // Detect touch to hide glow without interfering with page curl gesture + .simultaneousGesture( + DragGesture(minimumDistance: 0) + .onChanged { _ in + if !isInteracting { + isInteracting = true + } + } + ) + } else if squeezeJustCompleted && isFaceUp { + // After squeeze reveal: animate vertical flip to continue upward motion + VerticalFlipCardView(card: card, cardWidth: cardWidth) + } else { + // Standard CardView with flip animation + CardView(card: card, isFaceUp: isFaceUp, cardWidth: cardWidth) + .onTapGesture { + if !isFaceUp && isWaiting && revealStyle == .tap { + // Hide glow immediately before flip starts + isInteracting = true + onReveal() + } + } + } } } -// MARK: - Glow Modifier +// MARK: - Vertical Flip Card View -/// A view modifier that adds a pulsing glow effect around a card. -private struct GlowModifier: ViewModifier { +/// A card view that animates a vertical flip (X-axis rotation) to reveal the card face. +/// Used after the page curl peel completes to smoothly continue the upward flip motion. +private struct VerticalFlipCardView: View { + let card: Card + let cardWidth: CGFloat + + /// Rotation angle - starts slightly tilted and settles to flat + /// The page curl ends with the card nearly revealed, so we just do a small settling animation + @State private var rotationAngle: Double = -15 + + /// Opacity for smooth fade-in + @State private var opacity: Double = 0.8 + + private var cardHeight: CGFloat { + cardWidth * CasinoDesign.Size.cardAspectRatio + } + + var body: some View { + CardFrontView(card: card, width: cardWidth, height: cardHeight) + .rotation3DEffect( + .degrees(rotationAngle), + axis: (x: 1, y: 0, z: 0), + anchor: .bottom, + perspective: 0.4 + ) + .opacity(opacity) + .onAppear { + // Smooth settling animation + withAnimation(.easeOut(duration: 0.2)) { + rotationAngle = 0 + opacity = 1.0 + } + } + } +} + +// MARK: - Glow Background View + +/// A standalone glow view that sits underneath the card. +/// Completely separate from the card view to avoid any state changes affecting the card. +private struct GlowBackgroundView: View { let isActive: Bool let glowPulse: Bool let cardWidth: CGFloat @@ -72,44 +162,43 @@ private struct GlowModifier: ViewModifier { cardWidth * 0.08 } - func body(content: Content) -> some View { - if isActive { - content - .overlay { - // Glow effect using multiple stroke layers - ZStack { - // Outer glow strokes (largest to smallest for layered glow) - RoundedRectangle(cornerRadius: cornerRadius) - .stroke(Color.orange.opacity(glowPulse ? 0.6 : 0.2), lineWidth: glowPulse ? 20 : 10) - .blur(radius: 10) - - RoundedRectangle(cornerRadius: cornerRadius) - .stroke(Color.yellow.opacity(glowPulse ? 0.8 : 0.3), lineWidth: glowPulse ? 12 : 6) - .blur(radius: 6) - - RoundedRectangle(cornerRadius: cornerRadius) - .stroke(Color.yellow.opacity(glowPulse ? 1.0 : 0.5), lineWidth: glowPulse ? 6 : 3) - .blur(radius: 3) - - // Bright animated border (sharp, on top) - RoundedRectangle(cornerRadius: cornerRadius) - .strokeBorder( - LinearGradient( - colors: [.yellow, .white, .yellow], - startPoint: .topLeading, - endPoint: .bottomTrailing - ), - lineWidth: glowPulse ? 4 : 3 - ) - } - } - .scaleEffect(glowPulse ? 1.02 : 1.0) - .animation( - .easeInOut(duration: 0.7).repeatForever(autoreverses: true), - value: glowPulse + private var cardHeight: CGFloat { + cardWidth * CasinoDesign.Size.cardAspectRatio + } + + var body: some View { + ZStack { + // Outer glow strokes (largest to smallest for layered glow) + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(Color.orange.opacity(glowPulse ? 0.6 : 0.2), lineWidth: glowPulse ? 20 : 10) + .blur(radius: 10) + + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(Color.yellow.opacity(glowPulse ? 0.8 : 0.3), lineWidth: glowPulse ? 12 : 6) + .blur(radius: 6) + + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(Color.yellow.opacity(glowPulse ? 1.0 : 0.5), lineWidth: glowPulse ? 6 : 3) + .blur(radius: 3) + + // Bright animated border (sharp, on top) + RoundedRectangle(cornerRadius: cornerRadius) + .strokeBorder( + LinearGradient( + colors: [.yellow, .white, .yellow], + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + lineWidth: glowPulse ? 4 : 3 ) - } else { - content } + .frame(width: cardWidth, height: cardHeight) + .scaleEffect(isActive && glowPulse ? 1.02 : 1.0) + .opacity(isActive ? 1.0 : 0.0) + .animation(.easeOut(duration: 0.15), value: isActive) + .animation( + isActive ? .easeInOut(duration: 0.7).repeatForever(autoreverses: true) : .easeOut(duration: 0.15), + value: glowPulse + ) } } diff --git a/Baccarat/Baccarat/Views/Table/PageCurlView.swift b/Baccarat/Baccarat/Views/Table/PageCurlView.swift index 4340825..681700e 100644 --- a/Baccarat/Baccarat/Views/Table/PageCurlView.swift +++ b/Baccarat/Baccarat/Views/Table/PageCurlView.swift @@ -14,6 +14,8 @@ struct PageCurlView: View { let card: Card let width: CGFloat let onReveal: () -> Void + /// Called when user starts interacting with the page curl + var onInteractionStarted: (() -> Void)? = nil @State private var currentIndex = 0 @@ -22,7 +24,11 @@ struct PageCurlView: View { } var body: some View { - PageCurlRepresentable(currentIndex: $currentIndex, onReveal: onReveal, pages: [ + PageCurlRepresentable( + currentIndex: $currentIndex, + onReveal: onReveal, + onInteractionStarted: onInteractionStarted, + pages: [ // Page 0: Card Back (Top side) AnyView( CardBackView(width: width, height: height) @@ -49,6 +55,7 @@ struct PageCurlView: View { private struct PageCurlRepresentable: UIViewControllerRepresentable { @Binding var currentIndex: Int let onReveal: () -> Void + let onInteractionStarted: (() -> Void)? let pages: [AnyView] func makeUIViewController(context: Context) -> UIPageViewController { @@ -112,6 +119,12 @@ private struct PageCurlRepresentable: UIViewControllerRepresentable { return nextVC } + func pageViewController(_ pageVC: UIPageViewController, + willTransitionTo pendingViewControllers: [UIViewController]) { + // User started interacting with the page curl + parent.onInteractionStarted?() + } + func pageViewController(_ pageVC: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController],