192 lines
6.5 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|