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

222 lines
7.6 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 FilmAnimation5: View {
//
private let deviceWidth = UIScreen.main.bounds.width
private let deviceHeight = UIScreen.main.bounds.height
//
@State private var animationProgress: CGFloat = 0.0 // 0-1
@State private var isAnimating: Bool = false
@State private var animationComplete: Bool = false
//
private let reelImages: [[String]] = [
(0..<150).map { "film1-\($0+1)" }, //
(0..<180).map { "film2-\($0+1)" }, //
(0..<150).map { "film3-\($0+1)" } //
]
//
private let frameWidth: CGFloat = 90
private let frameHeight: CGFloat = 130
private let totalDistance: CGFloat = 1800 //
//
private let accelerationDuration: Double = 5.0 // 0-5s
private let constantSpeedDuration: Double = 1.0 // 5-6s
private let scaleDuration: Double = 2.0 // 6-8s
private var totalDuration: Double { accelerationDuration + constantSpeedDuration + scaleDuration }
//
private var accelerationEnd: CGFloat { accelerationDuration / totalDuration }
private var constantSpeedEnd: CGFloat { (accelerationDuration + constantSpeedDuration) / totalDuration }
//
private let symmetricTiltAngle: Double = 10 //
private let verticalOffset: CGFloat = 120 //
private let finalScale: CGFloat = 4.0 //
var body: some View {
ZStack {
//
Color(red: 0.08, green: 0.08, blue: 0.08)
.edgesIgnoringSafeArea(.all)
//
FilmReelView5(images: reelImages[0])
.rotationEffect(Angle(degrees: -symmetricTiltAngle))
.offset(x: topReelPosition, y: -verticalOffset)
.scaleEffect(currentScale)
.opacity(upperLowerOpacity)
.zIndex(2)
//
FilmReelView5(images: reelImages[2])
.rotationEffect(Angle(degrees: symmetricTiltAngle))
.offset(x: bottomReelPosition, y: verticalOffset)
.scaleEffect(currentScale)
.opacity(upperLowerOpacity)
.zIndex(2)
//
FilmReelView5(images: reelImages[1])
.offset(x: middleReelPosition, y: 0)
.scaleEffect(currentScale)
.opacity(1.0) //
.zIndex(1)
.edgesIgnoringSafeArea(.all)
}
.onAppear {
startAnimation()
}
}
// MARK: -
private func startAnimation() {
guard !isAnimating && !animationComplete else { return }
isAnimating = true
// 线
withAnimation(Animation.timingCurve(0.3, 0.0, 0.7, 1.0, duration: totalDuration)) {
animationProgress = 1.0
}
//
DispatchQueue.main.asyncAfter(deadline: .now() + totalDuration) {
isAnimating = false
animationComplete = true
}
}
// MARK: -
// 6s
private var currentScale: CGFloat {
guard animationProgress >= constantSpeedEnd else {
return 1.0 // 6s
}
// 0-1
let scalePhaseProgress = (animationProgress - constantSpeedEnd) / (1.0 - constantSpeedEnd)
return 1.0 + (finalScale - 1.0) * scalePhaseProgress
}
//
private var upperLowerOpacity: Double {
guard animationProgress >= constantSpeedEnd else {
return 0.8 // 6s
}
//
let fadeProgress = (animationProgress - constantSpeedEnd) / (1.0 - constantSpeedEnd)
return 0.8 * (1.0 - fadeProgress)
}
// MARK: -
private var motionProgress: CGFloat {
if animationProgress < accelerationEnd {
// 0-5s线
let t = animationProgress / accelerationEnd
return t * t
} else {
// 5s
return 1.0 + (animationProgress - accelerationEnd) *
(accelerationEnd / (1.0 - accelerationEnd))
}
}
//
private var topReelPosition: CGFloat {
totalDistance * 0.8 * motionProgress
}
//
private var middleReelPosition: CGFloat {
-totalDistance * 0.8 * motionProgress //
}
//
private var bottomReelPosition: CGFloat {
totalDistance * 0.8 * motionProgress //
}
}
// MARK: -
struct FilmReelView5: View {
let images: [String]
var body: some View {
HStack(spacing: 10) {
ForEach(images.indices, id: \.self) { index in
FilmFrameView5(imageName: images[index])
}
}
}
}
struct FilmFrameView5: View {
let imageName: String
var body: some View {
ZStack {
//
RoundedRectangle(cornerRadius: 4)
.stroke(Color.gray, lineWidth: 2)
.background(Color(red: 0.15, green: 0.15, blue: 0.15))
//
Rectangle()
.fill(gradientColor)
.cornerRadius(2)
.padding(2)
//
Text(imageName)
.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)
}
}
}
)
}
private var gradientColor: LinearGradient {
if imageName.hasPrefix("film1") {
return LinearGradient(gradient: Gradient(colors: [.blue, .indigo]), startPoint: .topLeading, endPoint: .bottomTrailing)
} else if imageName.hasPrefix("film2") {
return LinearGradient(gradient: Gradient(colors: [.yellow, .orange]), startPoint: .topLeading, endPoint: .bottomTrailing)
} else {
return LinearGradient(gradient: Gradient(colors: [.teal, .cyan]), startPoint: .topLeading, endPoint: .bottomTrailing)
}
}
}
//
struct FilmAnimation_Previews5: PreviewProvider {
static var previews: some View {
FilmAnimation5()
}
}