wake-ios/wake/View/Blind/Box1.swift
2025-08-22 18:58:08 +08:00

253 lines
8.8 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
struct ReplayableFilmReelAnimation: View {
//
@State private var animationProgress: CGFloat = 0
@State private var isCatching: Bool = false
@State private var isDisappearing: Bool = false
@State private var showReplayButton: Bool = false
// 16
private let reelImages: [[String]] = [
(0..<16).map { "film1-\($0+1)" }, //
(0..<16).map { "film2-\($0+1)" }, //
(0..<16).map { "film3-\($0+1)" } //
]
//
private let topTiltAngle: Double = -7 //
private let bottomTiltAngle: Double = 7 //
//
private let targetImageIndex: Int = 8
var body: some View {
ZStack {
//
Color(red: 0.08, green: 0.08, blue: 0.08).edgesIgnoringSafeArea(.all)
// -
ZStack {
//
FilmReelView1(images: reelImages[0])
.rotationEffect(Angle(degrees: topTiltAngle))
.offset(x: calculateTopOffset(), y: -200)
.opacity(isDisappearing ? 0 : 1)
.zIndex(1)
//
FilmReelView1(images: reelImages[1])
.offset(x: calculateMiddleOffset(), y: 0)
.scaleEffect(isCatching ? 1.03 : 1.0)
.opacity(isDisappearing ? 0 : 1)
.zIndex(2)
//
FilmReelView1(images: reelImages[2])
.rotationEffect(Angle(degrees: bottomTiltAngle))
.offset(x: calculateBottomOffset(), y: 200)
.opacity(isDisappearing ? 0 : 1)
.zIndex(1)
//
if isDisappearing {
Image(reelImages[1][targetImageIndex])
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
.transition(.opacity.combined(with: .scale))
.zIndex(3)
}
}
//
if showReplayButton {
Button(action: resetAndReplay) {
ZStack {
Circle()
.fill(Color.black.opacity(0.7))
.frame(width: 60, height: 60)
.shadow(radius: 10)
Image(systemName: "arrow.counterclockwise")
.foregroundColor(.white)
.font(.system(size: 24))
}
}
.position(x: UIScreen.main.bounds.width - 40, y: 40)
.transition(.opacity.combined(with: .scale))
.zIndex(4)
}
}
.onAppear {
startAnimation()
}
}
//
private func resetAndReplay() {
withAnimation(.easeInOut(duration: 0.5)) {
showReplayButton = false
isDisappearing = false
isCatching = false
animationProgress = 0
}
//
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
startAnimation()
}
}
//
private func calculateTopOffset() -> CGFloat {
let baseDistance: CGFloat = 1000
let speedFactor: CGFloat = 1.0
return baseDistance * speedFactor * progressCurve()
}
//
private func calculateMiddleOffset() -> CGFloat {
let baseDistance: CGFloat = -1100
let speedFactor: CGFloat = 1.05
return baseDistance * speedFactor * progressCurve()
}
//
private func calculateBottomOffset() -> CGFloat {
let baseDistance: CGFloat = 1000
let speedFactor: CGFloat = 0.95
return baseDistance * speedFactor * progressCurve()
}
// 线
private func progressCurve() -> CGFloat {
if animationProgress < 0.6 {
//
return easeInQuad(animationProgress / 0.6) * 0.7
} else if animationProgress < 0.85 {
//
return 0.7 + easeOutQuad((animationProgress - 0.6) / 0.25) * 0.25
} else {
//
let t = (animationProgress - 0.85) / 0.15
return 0.95 + t * 0.05
}
}
// 线
private func easeInQuad(_ t: CGFloat) -> CGFloat {
return t * t
}
// 线
private func easeOutQuad(_ t: CGFloat) -> CGFloat {
return t * (2 - t)
}
//
private func startAnimation() {
//
withAnimation(.easeIn(duration: 3.5)) {
animationProgress = 0.6
}
//
DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) {
withAnimation(.linear(duration: 2.5)) {
animationProgress = 0.85
}
//
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
withAnimation(.easeOut(duration: 1.8)) {
animationProgress = 1.0
isCatching = true
}
//
DispatchQueue.main.asyncAfter(deadline: .now() + 1.8) {
withAnimation(.easeInOut(duration: 0.7)) {
isDisappearing = true
}
//
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
withAnimation(.easeInOut(duration: 0.3)) {
showReplayButton = true
}
}
}
}
}
}
}
//
struct FilmReelView1: View {
let images: [String]
var body: some View {
HStack(spacing: 10) {
ForEach(images.indices, id: \.self) { index in
ZStack {
//
RoundedRectangle(cornerRadius: 4)
.stroke(Color.gray, lineWidth: 2)
.background(Color(red: 0.15, green: 0.15, blue: 0.15))
//
Rectangle()
.fill(
LinearGradient(
gradient: Gradient(colors: [.blue, .indigo]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.opacity(0.9)
.cornerRadius(2)
.padding(2)
//
Text("\(images[index])")
.foregroundColor(.white)
.font(.caption2)
}
.frame(width: 90, height: 130)
//
.overlay(
HStack {
VStack(spacing: 6) {
ForEach(0..<6) { _ in
Circle()
.frame(width: 6, height: 6)
.foregroundColor(.gray)
}
}
Spacer()
VStack(spacing: 6) {
ForEach(0..<6) { _ in
Circle()
.frame(width: 6, height: 6)
.foregroundColor(.gray)
}
}
}
)
}
}
}
}
//
struct ReplayableFilmReelAnimation_Previews: PreviewProvider {
static var previews: some View {
ReplayableFilmReelAnimation()
}
}