Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
b08acb9012
commit
109b2416fc
@ -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)
|
||||
private var cardHeight: CGFloat {
|
||||
cardWidth * CasinoDesign.Size.cardAspectRatio
|
||||
}
|
||||
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.stroke(Color.yellow.opacity(glowPulse ? 0.8 : 0.3), lineWidth: glowPulse ? 12 : 6)
|
||||
.blur(radius: 6)
|
||||
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 ? 1.0 : 0.5), lineWidth: glowPulse ? 6 : 3)
|
||||
.blur(radius: 3)
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.stroke(Color.yellow.opacity(glowPulse ? 0.8 : 0.3), lineWidth: glowPulse ? 12 : 6)
|
||||
.blur(radius: 6)
|
||||
|
||||
// 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
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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],
|
||||
|
||||
Loading…
Reference in New Issue
Block a user