226 lines
7.8 KiB
Swift
226 lines
7.8 KiB
Swift
import SwiftUI
|
||
|
||
struct FilmAnimation1: 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 frameSpacing: CGFloat = 10
|
||
private let totalDistance: CGFloat = 2000 // 总移动距离
|
||
|
||
// 动画时间参数
|
||
private let accelerationDuration: Double = 5.0 // 加速阶段时长(0-5s)
|
||
private let constantSpeedDuration: Double = 6.0 // 匀速+放大阶段时长(5-11s)
|
||
private var totalDuration: Double { accelerationDuration + constantSpeedDuration }
|
||
private var scaleStartProgress: CGFloat { accelerationDuration / totalDuration }
|
||
private let finalScale: CGFloat = 3.0 // 展示完整胶片的缩放比例
|
||
|
||
// 对称布局核心参数(重点调整)
|
||
private let symmetricTiltAngle: Double = 8 // 减小倾斜角度,增强对称感
|
||
private let verticalOffset: CGFloat = 140 // 减小垂直距离,靠近中间胶卷
|
||
private let initialMiddleY: CGFloat = 50 // 中间胶卷初始位置上移,缩短与上下距离
|
||
|
||
// 上下胶卷与中间胶卷的初始水平偏移(确保视觉对称)
|
||
private let horizontalOffset: CGFloat = 30
|
||
|
||
var body: some View {
|
||
ZStack {
|
||
// 深色背景
|
||
Color(red: 0.08, green: 0.08, blue: 0.08)
|
||
.edgesIgnoringSafeArea(.all)
|
||
|
||
// 上方倾斜胶卷(左高右低,与中间距离适中)
|
||
FilmReelView3(images: reelImages[0])
|
||
.rotationEffect(Angle(degrees: -symmetricTiltAngle))
|
||
.offset(x: topReelPosition - horizontalOffset, y: -verticalOffset) // 水平微调增强对称
|
||
.opacity(upperLowerOpacity)
|
||
.zIndex(1)
|
||
|
||
// 下方倾斜胶卷(左低右高,与中间距离适中)
|
||
FilmReelView3(images: reelImages[2])
|
||
.rotationEffect(Angle(degrees: symmetricTiltAngle))
|
||
.offset(x: bottomReelPosition + horizontalOffset, y: verticalOffset) // 水平微调增强对称
|
||
.opacity(upperLowerOpacity)
|
||
.zIndex(1)
|
||
|
||
// 中间胶卷(垂直居中)
|
||
FilmReelView3(images: reelImages[1])
|
||
.offset(x: middleReelPosition, y: middleYPosition)
|
||
.scaleEffect(currentScale)
|
||
.position(centerPosition)
|
||
.zIndex(2)
|
||
.edgesIgnoringSafeArea(.all)
|
||
}
|
||
.onAppear {
|
||
startAnimation()
|
||
}
|
||
}
|
||
|
||
// MARK: - 动画逻辑
|
||
|
||
private func startAnimation() {
|
||
guard !isAnimating && !animationComplete else { return }
|
||
isAnimating = true
|
||
|
||
withAnimation(Animation.timingCurve(0.2, 0.0, 0.8, 1.0, duration: totalDuration)) {
|
||
animationProgress = 1.0
|
||
}
|
||
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + totalDuration) {
|
||
isAnimating = false
|
||
animationComplete = true
|
||
}
|
||
}
|
||
|
||
// MARK: - 动画计算
|
||
|
||
private var currentScale: CGFloat {
|
||
guard animationProgress >= scaleStartProgress else {
|
||
return 1.0
|
||
}
|
||
|
||
let scalePhaseProgress = (animationProgress - scaleStartProgress) / (1.0 - scaleStartProgress)
|
||
return 1.0 + (finalScale - 1.0) * scalePhaseProgress
|
||
}
|
||
|
||
// 中间胶卷Y轴位置(微调至更居中)
|
||
private var middleYPosition: CGFloat {
|
||
if animationProgress < scaleStartProgress {
|
||
return initialMiddleY - (initialMiddleY * (animationProgress / scaleStartProgress))
|
||
} else {
|
||
return 0 // 5s后精准居中
|
||
}
|
||
}
|
||
|
||
private var upperLowerOpacity: Double {
|
||
if animationProgress < scaleStartProgress {
|
||
return 0.8
|
||
} else {
|
||
let fadeProgress = (animationProgress - scaleStartProgress) / (1.0 - scaleStartProgress)
|
||
return 0.8 * (1.0 - fadeProgress)
|
||
}
|
||
}
|
||
|
||
private var centerPosition: CGPoint {
|
||
CGPoint(x: deviceWidth / 2, y: deviceHeight / 2)
|
||
}
|
||
|
||
// MARK: - 位置计算(确保对称运动)
|
||
|
||
private var motionProgress: CGFloat {
|
||
if animationProgress < scaleStartProgress {
|
||
let t = animationProgress / scaleStartProgress
|
||
return t * t // 加速阶段
|
||
} else {
|
||
return 1.0 + (animationProgress - scaleStartProgress) *
|
||
(scaleStartProgress / (1.0 - scaleStartProgress))
|
||
}
|
||
}
|
||
|
||
// 上方胶卷位置(与下方保持对称速度)
|
||
private var topReelPosition: CGFloat {
|
||
totalDistance * 0.9 * motionProgress
|
||
}
|
||
|
||
// 中间胶卷位置(主视觉移动)
|
||
private var middleReelPosition: CGFloat {
|
||
-totalDistance * 1.2 * motionProgress
|
||
}
|
||
|
||
// 下方胶卷位置(与上方保持对称速度)
|
||
private var bottomReelPosition: CGFloat {
|
||
totalDistance * 0.9 * motionProgress // 与上方速度完全一致
|
||
}
|
||
}
|
||
|
||
// MARK: - 胶卷组件
|
||
|
||
struct FilmReelView3: View {
|
||
let images: [String]
|
||
|
||
var body: some View {
|
||
HStack(spacing: 10) {
|
||
ForEach(images.indices, id: \.self) { index in
|
||
FilmFrameView3(imageName: images[index])
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
struct FilmFrameView3: 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_Previews3: PreviewProvider {
|
||
static var previews: some View {
|
||
FilmAnimation1()
|
||
}
|
||
}
|
||
|