CasinoGames/Baccarat/Baccarat/Views/Table/SqueezeCardView.swift
Matt Bruce 833841dcdd page curl
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
2026-01-21 17:03:46 -06:00

192 lines
6.5 KiB
Swift

//
// SqueezeCardView.swift
// Baccarat
//
// A premium card view that allows for a gestural "squeeze" or "peel" reveal,
// mimicking a real-world page flip from the corners.
//
import SwiftUI
import CasinoKit
enum SqueezeCorner {
case bottomLeft
case bottomRight
}
struct SqueezeCardView: View {
let card: Card
let width: CGFloat
let progress: Double // 0.0 to 1.0
let corner: SqueezeCorner
private var height: CGFloat {
width * CasinoDesign.Size.cardAspectRatio
}
var body: some View {
ZStack {
// 1. The front is always at the bottom
CardFrontView(card: card, width: width, height: height)
// 2. The back is on top, masked by the "unpeeled" area
CardBackView(width: width, height: height)
.mask(
SqueezeMask(progress: progress, corner: corner)
.fill(Style.maskFill)
)
// 3. The "Fold" - visual affordance of the peeled corner
if progress > 0 && progress < 1.0 {
FoldEffect(progress: progress, corner: corner, width: width, height: height)
}
}
.frame(width: width, height: height)
.clipShape(RoundedRectangle(cornerRadius: width * 0.05))
}
private enum Style {
static let maskFill = Color.black
}
}
/// A mask that represents the part of the card back that is NOT yet peeled.
struct SqueezeMask: Shape {
var progress: Double
var corner: SqueezeCorner
var animatableData: Double {
get { progress }
set { progress = newValue }
}
func path(in rect: CGRect) -> Path {
var path = Path()
let p = CGFloat(progress)
// Initial state: Full rectangle
path.addRect(rect)
// Subtraction: The "peeled away" triangle
var peelPath = Path()
let size = rect.width * p * 2.0 // Scaling factor for the peel area
switch corner {
case .bottomRight:
peelPath.move(to: CGPoint(x: rect.width, y: rect.height))
peelPath.addLine(to: CGPoint(x: rect.width - size, y: rect.height))
peelPath.addLine(to: CGPoint(x: rect.width, y: rect.height - size))
peelPath.closeSubpath()
case .bottomLeft:
peelPath.move(to: CGPoint(x: 0, y: rect.height))
peelPath.addLine(to: CGPoint(x: size, y: rect.height))
peelPath.addLine(to: CGPoint(x: 0, y: rect.height - size))
peelPath.closeSubpath()
}
// We use a "subtract" logic by drawing the full rect then "erasing" the peel
// In SwiftUI Shape, we just return the "remaining" area.
// For simplicity, we'll construct the remaining polygon.
var remainingPath = Path()
switch corner {
case .bottomRight:
remainingPath.move(to: .zero)
remainingPath.addLine(to: CGPoint(x: rect.width, y: 0))
remainingPath.addLine(to: CGPoint(x: rect.width, y: max(0, rect.height - size)))
remainingPath.addLine(to: CGPoint(x: max(0, rect.width - size), y: rect.height))
remainingPath.addLine(to: CGPoint(x: 0, y: rect.height))
remainingPath.closeSubpath()
case .bottomLeft:
remainingPath.move(to: .zero)
remainingPath.addLine(to: CGPoint(x: rect.width, y: 0))
remainingPath.addLine(to: CGPoint(x: rect.width, y: rect.height))
remainingPath.addLine(to: CGPoint(x: min(rect.width, size), y: rect.height))
remainingPath.addLine(to: CGPoint(x: 0, y: max(0, rect.height - size)))
remainingPath.closeSubpath()
}
return remainingPath
}
}
/// The visual effect of the folded corner, including shadows and highlight.
struct FoldEffect: View {
let progress: Double
let corner: SqueezeCorner
let width: CGFloat
let height: CGFloat
var body: some View {
let size = width * CGFloat(progress) * 2.0
let foldWidth = size * 0.5
ZStack {
// Fold shadow cast on the front of the card
FoldShadow(size: size, corner: corner)
.opacity(0.3 * (1.0 - progress))
// The fold itself (the "back" of the peel)
FoldShape(size: size, corner: corner)
.fill(
LinearGradient(
colors: [.white.opacity(0.8), .gray.opacity(0.3)],
startPoint: corner == .bottomRight ? .bottomTrailing : .bottomLeading,
endPoint: corner == .bottomRight ? .topLeading : .topTrailing
)
)
.overlay(
FoldShape(size: size, corner: corner)
.stroke(Color.white.opacity(0.5), lineWidth: 0.5)
)
}
}
}
struct FoldShape: Shape {
let size: CGFloat
let corner: SqueezeCorner
func path(in rect: CGRect) -> Path {
var path = Path()
// A triangle representing the folded over piece
switch corner {
case .bottomRight:
path.move(to: CGPoint(x: rect.width - size/2, y: rect.height - size/2))
path.addLine(to: CGPoint(x: rect.width - size, y: rect.height))
path.addLine(to: CGPoint(x: rect.width, y: rect.height - size))
path.closeSubpath()
case .bottomLeft:
path.move(to: CGPoint(x: size/2, y: rect.height - size/2))
path.addLine(to: CGPoint(x: size, y: rect.height))
path.addLine(to: CGPoint(x: 0, y: rect.height - size))
path.closeSubpath()
}
return path
}
}
struct FoldShadow: View {
let size: CGFloat
let corner: SqueezeCorner
var body: some View {
FoldShape(size: size * 1.1, corner: corner)
.fill(Color.black)
.blur(radius: 4)
.offset(x: corner == .bottomRight ? -2 : 2, y: -2)
}
}
#Preview {
ZStack {
Color.CasinoTable.backgroundDark.ignoresSafeArea()
VStack(spacing: 60) {
SqueezeCardView(card: Card(suit: .spades, rank: .ace), width: 140, progress: 0.1, corner: .bottomRight)
SqueezeCardView(card: Card(suit: .hearts, rank: .king), width: 140, progress: 0.3, corner: .bottomRight)
SqueezeCardView(card: Card(suit: .diamonds, rank: .seven), width: 140, progress: 0.5, corner: .bottomLeft)
}
}
}