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

This commit is contained in:
Matt Bruce 2026-01-24 16:29:44 -06:00
parent b08acb9012
commit 109b2416fc
2 changed files with 161 additions and 59 deletions

View File

@ -20,33 +20,36 @@ struct InteractiveCardView: View {
let onReveal: () -> Void let onReveal: () -> Void
let onUpdateProgress: (Double) -> 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 /// 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 { private var showGlow: Bool {
isWaiting && !isFaceUp && isBottomHand && revealStyle != .auto isWaiting && !isFaceUp && isBottomHand && revealStyle != .auto && !isInteracting
} }
/// Animation state for the pulsing glow /// Animation state for the pulsing glow
@State private var glowPulse = false @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 { var body: some View {
ZStack { ZStack {
if revealStyle == .squeeze && !isFaceUp && isWaiting { // Glow layer - completely separate from the card, sits underneath
PageCurlView( GlowBackgroundView(
card: card, isActive: showGlow,
width: cardWidth, glowPulse: glowPulse,
onReveal: onReveal cardWidth: cardWidth
) )
.modifier(GlowModifier(isActive: showGlow, glowPulse: glowPulse, cardWidth: cardWidth))
} else { // Card layer - completely independent, no glow modifiers
// Standard CardView (handles its own flip animation) cardContent
CardView(card: card, isFaceUp: isFaceUp, cardWidth: cardWidth)
.modifier(GlowModifier(isActive: showGlow, glowPulse: glowPulse, cardWidth: cardWidth))
.onTapGesture {
if !isFaceUp && isWaiting && revealStyle == .tap {
onReveal()
}
}
}
} }
.onAppear { .onAppear {
if showGlow { if showGlow {
@ -56,13 +59,100 @@ struct InteractiveCardView: View {
.onChange(of: showGlow) { _, newValue in .onChange(of: showGlow) { _, newValue in
glowPulse = newValue 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. /// A card view that animates a vertical flip (X-axis rotation) to reveal the card face.
private struct GlowModifier: ViewModifier { /// 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 isActive: Bool
let glowPulse: Bool let glowPulse: Bool
let cardWidth: CGFloat let cardWidth: CGFloat
@ -72,44 +162,43 @@ private struct GlowModifier: ViewModifier {
cardWidth * 0.08 cardWidth * 0.08
} }
func body(content: Content) -> some View { private var cardHeight: CGFloat {
if isActive { cardWidth * CasinoDesign.Size.cardAspectRatio
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) var body: some View {
.stroke(Color.yellow.opacity(glowPulse ? 0.8 : 0.3), lineWidth: glowPulse ? 12 : 6) ZStack {
.blur(radius: 6) // 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) RoundedRectangle(cornerRadius: cornerRadius)
.stroke(Color.yellow.opacity(glowPulse ? 1.0 : 0.5), lineWidth: glowPulse ? 6 : 3) .stroke(Color.yellow.opacity(glowPulse ? 0.8 : 0.3), lineWidth: glowPulse ? 12 : 6)
.blur(radius: 3) .blur(radius: 6)
// Bright animated border (sharp, on top) RoundedRectangle(cornerRadius: cornerRadius)
RoundedRectangle(cornerRadius: cornerRadius) .stroke(Color.yellow.opacity(glowPulse ? 1.0 : 0.5), lineWidth: glowPulse ? 6 : 3)
.strokeBorder( .blur(radius: 3)
LinearGradient(
colors: [.yellow, .white, .yellow], // Bright animated border (sharp, on top)
startPoint: .topLeading, RoundedRectangle(cornerRadius: cornerRadius)
endPoint: .bottomTrailing .strokeBorder(
), LinearGradient(
lineWidth: glowPulse ? 4 : 3 colors: [.yellow, .white, .yellow],
) startPoint: .topLeading,
} endPoint: .bottomTrailing
} ),
.scaleEffect(glowPulse ? 1.02 : 1.0) lineWidth: glowPulse ? 4 : 3
.animation(
.easeInOut(duration: 0.7).repeatForever(autoreverses: true),
value: glowPulse
) )
} 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
)
} }
} }

View File

@ -14,6 +14,8 @@ struct PageCurlView: View {
let card: Card let card: Card
let width: CGFloat let width: CGFloat
let onReveal: () -> Void let onReveal: () -> Void
/// Called when user starts interacting with the page curl
var onInteractionStarted: (() -> Void)? = nil
@State private var currentIndex = 0 @State private var currentIndex = 0
@ -22,7 +24,11 @@ struct PageCurlView: View {
} }
var body: some 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) // Page 0: Card Back (Top side)
AnyView( AnyView(
CardBackView(width: width, height: height) CardBackView(width: width, height: height)
@ -49,6 +55,7 @@ struct PageCurlView: View {
private struct PageCurlRepresentable: UIViewControllerRepresentable { private struct PageCurlRepresentable: UIViewControllerRepresentable {
@Binding var currentIndex: Int @Binding var currentIndex: Int
let onReveal: () -> Void let onReveal: () -> Void
let onInteractionStarted: (() -> Void)?
let pages: [AnyView] let pages: [AnyView]
func makeUIViewController(context: Context) -> UIPageViewController { func makeUIViewController(context: Context) -> UIPageViewController {
@ -112,6 +119,12 @@ private struct PageCurlRepresentable: UIViewControllerRepresentable {
return nextVC return nextVC
} }
func pageViewController(_ pageVC: UIPageViewController,
willTransitionTo pendingViewControllers: [UIViewController]) {
// User started interacting with the page curl
parent.onInteractionStarted?()
}
func pageViewController(_ pageVC: UIPageViewController, func pageViewController(_ pageVC: UIPageViewController,
didFinishAnimating finished: Bool, didFinishAnimating finished: Bool,
previousViewControllers: [UIViewController], previousViewControllers: [UIViewController],