222 lines
7.6 KiB
Swift
222 lines
7.6 KiB
Swift
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()
|
||
}
|
||
}
|
||
|