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 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
// Glow layer - completely separate from the card, sits underneath
GlowBackgroundView(
isActive: showGlow,
glowPulse: glowPulse,
cardWidth: cardWidth
)
.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()
}
}
}
// 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,11 +162,11 @@ private struct GlowModifier: ViewModifier {
cardWidth * 0.08
}
func body(content: Content) -> some View {
if isActive {
content
.overlay {
// Glow effect using multiple stroke layers
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)
@ -102,14 +192,13 @@ private struct GlowModifier: ViewModifier {
lineWidth: glowPulse ? 4 : 3
)
}
}
.scaleEffect(glowPulse ? 1.02 : 1.0)
.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(
.easeInOut(duration: 0.7).repeatForever(autoreverses: true),
isActive ? .easeInOut(duration: 0.7).repeatForever(autoreverses: true) : .easeOut(duration: 0.15),
value: glowPulse
)
} else {
content
}
}
}

View File

@ -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],