310 lines
9.3 KiB
Swift
310 lines
9.3 KiB
Swift
import SwiftUI
|
|
|
|
struct GachaAnimationView: View {
|
|
let drawResult: CardDrawResult
|
|
let onComplete: () -> Void
|
|
|
|
@State private var phase: AnimationPhase = .opening
|
|
@State private var packScale: CGFloat = 0
|
|
@State private var packOpacity: Double = 0
|
|
@State private var cardScale: CGFloat = 0
|
|
@State private var cardOpacity: Double = 0
|
|
@State private var showCard = false
|
|
@State private var effectOpacity: Double = 0
|
|
|
|
enum AnimationPhase {
|
|
case opening
|
|
case revealing
|
|
case complete
|
|
}
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Dark overlay
|
|
Rectangle()
|
|
.fill(Color.black.opacity(0.9))
|
|
.ignoresSafeArea()
|
|
.onTapGesture {
|
|
if phase == .complete {
|
|
onComplete()
|
|
}
|
|
}
|
|
|
|
// Background effects based on card rarity
|
|
if phase != .opening {
|
|
backgroundEffect
|
|
.opacity(effectOpacity)
|
|
}
|
|
|
|
// Pack animation
|
|
if phase == .opening {
|
|
GachaPackView()
|
|
.scaleEffect(packScale)
|
|
.opacity(packOpacity)
|
|
}
|
|
|
|
// Card reveal
|
|
if showCard {
|
|
CardView(card: drawResult.card, isRevealing: true)
|
|
.scaleEffect(cardScale)
|
|
.opacity(cardOpacity)
|
|
}
|
|
|
|
// Complete state overlay
|
|
if phase == .complete {
|
|
VStack {
|
|
Spacer()
|
|
|
|
VStack(spacing: 16) {
|
|
if drawResult.isNew {
|
|
Text("新しいカードを獲得!")
|
|
.font(.title2)
|
|
.fontWeight(.bold)
|
|
.foregroundColor(Color(hex: "fff700"))
|
|
}
|
|
|
|
Text("タップして続ける")
|
|
.font(.caption)
|
|
.foregroundColor(.white.opacity(0.7))
|
|
}
|
|
.padding(.bottom, 50)
|
|
}
|
|
}
|
|
}
|
|
.onAppear {
|
|
startAnimation()
|
|
}
|
|
}
|
|
|
|
private var backgroundEffect: some View {
|
|
Group {
|
|
switch drawResult.animationType {
|
|
case "unique":
|
|
UniqueBackgroundEffect()
|
|
case "kira":
|
|
KiraBackgroundEffect()
|
|
case "rare":
|
|
RareBackgroundEffect()
|
|
default:
|
|
EmptyView()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func startAnimation() {
|
|
// Phase 1: Pack appears
|
|
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
|
|
packScale = 1.0
|
|
packOpacity = 1.0
|
|
}
|
|
|
|
// Phase 2: Pack disappears, card appears
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
|
|
withAnimation(.easeOut(duration: 0.3)) {
|
|
packOpacity = 0
|
|
}
|
|
|
|
phase = .revealing
|
|
showCard = true
|
|
|
|
withAnimation(.spring(response: 0.8, dampingFraction: 0.6)) {
|
|
cardScale = 1.0
|
|
cardOpacity = 1.0
|
|
effectOpacity = 1.0
|
|
}
|
|
}
|
|
|
|
// Phase 3: Animation complete
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) {
|
|
phase = .complete
|
|
}
|
|
}
|
|
}
|
|
|
|
struct GachaPackView: View {
|
|
@State private var glowIntensity: Double = 0.5
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Pack background
|
|
RoundedRectangle(cornerRadius: 20)
|
|
.fill(
|
|
LinearGradient(
|
|
gradient: Gradient(colors: [
|
|
Color(hex: "667eea"),
|
|
Color(hex: "764ba2")
|
|
]),
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
)
|
|
.frame(width: 150, height: 200)
|
|
|
|
// Pack glow
|
|
RoundedRectangle(cornerRadius: 20)
|
|
.stroke(Color.white, lineWidth: 2)
|
|
.frame(width: 150, height: 200)
|
|
.blur(radius: 10)
|
|
.opacity(glowIntensity)
|
|
|
|
// Pack label
|
|
VStack {
|
|
Image(systemName: "sparkles")
|
|
.font(.title)
|
|
.foregroundColor(.white)
|
|
|
|
Text("ai.card")
|
|
.font(.headline)
|
|
.fontWeight(.bold)
|
|
.foregroundColor(.white)
|
|
}
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 1).repeatForever(autoreverses: true)) {
|
|
glowIntensity = 1.0
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct UniqueBackgroundEffect: View {
|
|
@State private var particles: [ParticleData] = []
|
|
@State private var burstScale: CGFloat = 0
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Radial burst
|
|
Circle()
|
|
.fill(
|
|
RadialGradient(
|
|
gradient: Gradient(colors: [
|
|
Color(hex: "ff00ff").opacity(0.8),
|
|
Color.clear
|
|
]),
|
|
center: .center,
|
|
startRadius: 0,
|
|
endRadius: 200
|
|
)
|
|
)
|
|
.scaleEffect(burstScale)
|
|
.onAppear {
|
|
withAnimation(.easeOut(duration: 1)) {
|
|
burstScale = 3
|
|
}
|
|
}
|
|
|
|
// Floating particles
|
|
ForEach(particles, id: \.id) { particle in
|
|
Circle()
|
|
.fill(Color(hex: particle.color))
|
|
.frame(width: particle.size, height: particle.size)
|
|
.position(x: particle.x, y: particle.y)
|
|
.opacity(particle.opacity)
|
|
}
|
|
}
|
|
.onAppear {
|
|
generateParticles()
|
|
}
|
|
}
|
|
|
|
private func generateParticles() {
|
|
for i in 0..<20 {
|
|
let particle = ParticleData(
|
|
id: i,
|
|
x: CGFloat.random(in: 0...UIScreen.main.bounds.width),
|
|
y: CGFloat.random(in: 0...UIScreen.main.bounds.height),
|
|
size: CGFloat.random(in: 4...12),
|
|
color: ["ff00ff", "00ffff", "ffffff"].randomElement() ?? "ffffff",
|
|
opacity: Double.random(in: 0.3...0.8)
|
|
)
|
|
particles.append(particle)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct KiraBackgroundEffect: View {
|
|
@State private var sparkleOffset: CGFloat = -100
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
ForEach(0..<5, id: \.self) { i in
|
|
Rectangle()
|
|
.fill(
|
|
LinearGradient(
|
|
gradient: Gradient(colors: [
|
|
Color.clear,
|
|
Color.yellow.opacity(0.3),
|
|
Color.clear
|
|
]),
|
|
startPoint: .leading,
|
|
endPoint: .trailing
|
|
)
|
|
)
|
|
.frame(width: 2, height: UIScreen.main.bounds.height)
|
|
.rotationEffect(.degrees(45))
|
|
.offset(x: sparkleOffset + CGFloat(i * 50))
|
|
}
|
|
}
|
|
.onAppear {
|
|
withAnimation(.linear(duration: 2).repeatForever(autoreverses: false)) {
|
|
sparkleOffset = UIScreen.main.bounds.width + 100
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct RareBackgroundEffect: View {
|
|
@State private var rippleScale: CGFloat = 0
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
ForEach(0..<3, id: \.self) { i in
|
|
Circle()
|
|
.stroke(Color.blue.opacity(0.3), lineWidth: 2)
|
|
.scaleEffect(rippleScale)
|
|
.opacity(1 - rippleScale)
|
|
.animation(
|
|
.easeOut(duration: 2)
|
|
.delay(Double(i) * 0.3)
|
|
.repeatForever(autoreverses: false),
|
|
value: rippleScale
|
|
)
|
|
}
|
|
}
|
|
.onAppear {
|
|
rippleScale = 3
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ParticleData {
|
|
let id: Int
|
|
let x: CGFloat
|
|
let y: CGFloat
|
|
let size: CGFloat
|
|
let color: String
|
|
let opacity: Double
|
|
}
|
|
|
|
struct GachaAnimationView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
let sampleResult = CardDrawResult(
|
|
card: Card(
|
|
id: 0,
|
|
cp: 500,
|
|
status: .unique,
|
|
skill: "サンプルスキル",
|
|
ownerDid: "did:plc:example",
|
|
obtainedAt: Date(),
|
|
isUnique: true,
|
|
uniqueId: "unique-123"
|
|
),
|
|
isNew: true,
|
|
animationType: "unique"
|
|
)
|
|
|
|
GachaAnimationView(drawResult: sampleResult) {
|
|
print("Animation complete")
|
|
}
|
|
}
|
|
} |