feat: 加速后匀速
This commit is contained in:
parent
0aa1271c93
commit
949b371b49
@ -3,214 +3,162 @@ import SwiftUI
|
|||||||
struct FilmAnimation: View {
|
struct FilmAnimation: View {
|
||||||
// 设备尺寸
|
// 设备尺寸
|
||||||
private let deviceWidth = UIScreen.main.bounds.width
|
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 animationProgress: CGFloat = 0.0
|
||||||
@State private var isAnimating: Bool = false
|
@State private var hasAccelerated: Bool = false // 标记是否完成第一遍加速
|
||||||
@State private var animationComplete: Bool = false
|
|
||||||
|
|
||||||
// 胶卷数据
|
// 胶卷数据(16帧)
|
||||||
private let reelImages: [[String]] = [
|
private let reelFrames: Int = 16
|
||||||
(0..<300).map { "film1-\($0+1)" }, // 上方胶卷
|
private var reel1: [String] { (0..<reelFrames).map { "film1-\($0+1)" } }
|
||||||
(0..<350).map { "film2-\($0+1)" }, // 中间胶卷
|
private var reel2: [String] { (0..<reelFrames).map { "film2-\($0+1)" } }
|
||||||
(0..<300).map { "film3-\($0+1)" } // 下方胶卷
|
private var reel3: [String] { (0..<reelFrames).map { "film3-\($0+1)" } }
|
||||||
]
|
|
||||||
|
|
||||||
// 胶卷参数
|
// 胶卷参数(轮播图关键参数)
|
||||||
private let frameWidth: CGFloat = 90
|
private let frameWidth: CGFloat = 90
|
||||||
private let frameHeight: CGFloat = 130
|
private let frameHeight: CGFloat = 130
|
||||||
private let frameSpacing: CGFloat = 12
|
private let frameSpacing: CGFloat = 12
|
||||||
|
private var frameTotalWidth: CGFloat { frameWidth + frameSpacing }
|
||||||
|
private var visibleFrames: Int { Int(deviceWidth / frameTotalWidth) + 2 } // 可见帧数
|
||||||
|
|
||||||
// 动画阶段时间参数
|
// 动画时间参数
|
||||||
private let accelerationDuration: Double = 5.0 // 0-5s加速
|
private let accelerationDuration: Double = 3.0 // 加速时间
|
||||||
private let constantSpeedDuration: Double = 1.0 // 5-6s匀速
|
private let cycleDuration: Double = 2.0 // 匀速循环周期
|
||||||
private let scaleStartDuration: Double = 1.0 // 6-7s共同放大
|
|
||||||
private let scaleFinishDuration: Double = 1.0 // 7-8s仅中间胶卷放大
|
|
||||||
private var totalDuration: Double {
|
|
||||||
accelerationDuration + constantSpeedDuration + scaleStartDuration + scaleFinishDuration
|
|
||||||
}
|
|
||||||
|
|
||||||
// 各阶段进度阈值
|
// 布局与运动参数
|
||||||
private var accelerationEnd: CGFloat { accelerationDuration / totalDuration }
|
private let upperTilt: Double = -10 // 上卷左高右低
|
||||||
private var constantSpeedEnd: CGFloat { (accelerationDuration + constantSpeedDuration) / totalDuration }
|
private let lowerTilt: Double = 10 // 下卷左低右高
|
||||||
private var scaleStartEnd: CGFloat {
|
private let verticalSpacing: CGFloat = 200
|
||||||
(accelerationDuration + constantSpeedDuration + scaleStartDuration) / totalDuration
|
|
||||||
}
|
|
||||||
|
|
||||||
// 布局与运动参数(核心:对称倾斜角度)
|
// 轮播关键参数(轨道长度 = 单组帧总宽度)
|
||||||
private let tiltAngle: Double = 10 // 基础倾斜角度
|
private var trackLength: CGFloat { CGFloat(reelFrames) * frameTotalWidth }
|
||||||
private let upperTilt: Double = -10 // 上方胶卷:左高右低(负角度)
|
|
||||||
private let lowerTilt: Double = 10 // 下方胶卷:左低右高(正角度)
|
|
||||||
private let verticalSpacing: CGFloat = 200 // 上下胶卷垂直间距
|
|
||||||
private let finalScale: CGFloat = 4.5
|
|
||||||
|
|
||||||
// 移动距离参数
|
|
||||||
private let maxTiltedReelMovement: CGFloat = 3500 // 倾斜胶卷最大移动距离
|
|
||||||
private let maxMiddleReelMovement: CGFloat = -3000 // 中间胶卷最大移动距离
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
// 固定背景
|
|
||||||
Color(red: 0.08, green: 0.08, blue: 0.08)
|
|
||||||
.edgesIgnoringSafeArea(.all)
|
|
||||||
.overlay(
|
|
||||||
ZStack {
|
ZStack {
|
||||||
// 上方倾斜胶卷(左高右低,向右移动)
|
// 上方胶卷(右向轮播)
|
||||||
if showTiltedReels {
|
FilmCarouselView(images: reel1)
|
||||||
FilmReelView(images: reelImages[0])
|
.rotationEffect(Angle(degrees: upperTilt))
|
||||||
.rotationEffect(Angle(degrees: upperTilt)) // 左高右低
|
.offset(x: upperTrackPosition, y: -verticalSpacing/2)
|
||||||
.offset(x: upperReelXPosition, y: -verticalSpacing/2)
|
|
||||||
.scaleEffect(tiltedScale)
|
|
||||||
.opacity(tiltedOpacity)
|
|
||||||
.zIndex(1)
|
.zIndex(1)
|
||||||
}
|
|
||||||
|
|
||||||
// 下方倾斜胶卷(左低右高,向右移动)
|
// 下方胶卷(右向轮播)
|
||||||
if showTiltedReels {
|
FilmCarouselView(images: reel3)
|
||||||
FilmReelView(images: reelImages[2])
|
.rotationEffect(Angle(degrees: lowerTilt))
|
||||||
.rotationEffect(Angle(degrees: lowerTilt)) // 左低右高
|
.offset(x: lowerTrackPosition, y: verticalSpacing/2)
|
||||||
.offset(x: lowerReelXPosition, y: verticalSpacing/2)
|
|
||||||
.scaleEffect(tiltedScale)
|
|
||||||
.opacity(tiltedOpacity)
|
|
||||||
.zIndex(1)
|
.zIndex(1)
|
||||||
}
|
|
||||||
|
|
||||||
// 中间胶卷(垂直,向左移动)
|
// 中间胶卷(左向轮播)
|
||||||
FilmReelView(images: reelImages[1])
|
FilmCarouselView(images: reel2)
|
||||||
.offset(x: middleReelXPosition, y: 0)
|
.offset(x: middleTrackPosition, y: 0)
|
||||||
.scaleEffect(middleScale)
|
|
||||||
.opacity(1.0)
|
|
||||||
.zIndex(2)
|
.zIndex(2)
|
||||||
.edgesIgnoringSafeArea(.all)
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
startAnimation()
|
startCarouselAnimation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 动画逻辑
|
// MARK: - 轮播动画控制(核心轮播逻辑)
|
||||||
|
|
||||||
private func startAnimation() {
|
private func startCarouselAnimation() {
|
||||||
guard !isAnimating && !animationComplete else { return }
|
// 第一阶段:加速动画
|
||||||
isAnimating = true
|
withAnimation(Animation.linear(duration: accelerationDuration)) {
|
||||||
|
|
||||||
withAnimation(Animation.easeInOut(duration: totalDuration)) {
|
|
||||||
animationProgress = 1.0
|
animationProgress = 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + totalDuration) {
|
// 加速完成后进入匀速循环
|
||||||
isAnimating = false
|
DispatchQueue.main.asyncAfter(deadline: .now() + accelerationDuration) {
|
||||||
animationComplete = true
|
hasAccelerated = true
|
||||||
|
// 匀速循环动画
|
||||||
|
withAnimation(Animation.linear(duration: cycleDuration).repeatForever(autoreverses: false)) {
|
||||||
|
animationProgress = 2.0 // 从1.0到2.0实现一次循环
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 位置计算(确保向右移动)
|
// MARK: - 速度计算(加速到匀速过渡)
|
||||||
|
|
||||||
// 上方倾斜胶卷X位置
|
private var effectiveProgress: CGFloat {
|
||||||
private var upperReelXPosition: CGFloat {
|
if !hasAccelerated {
|
||||||
let startPosition: CGFloat = -deviceWidth * 1.2 // 左侧屏幕外起始
|
// 加速阶段:0→1(三次方曲线加速)
|
||||||
return startPosition + (maxTiltedReelMovement * movementProgress)
|
return animationProgress * animationProgress * animationProgress
|
||||||
}
|
|
||||||
|
|
||||||
// 下方倾斜胶卷X位置
|
|
||||||
private var lowerReelXPosition: CGFloat {
|
|
||||||
let startPosition: CGFloat = -deviceWidth * 0.8 // 稍右于上方胶卷起始
|
|
||||||
return startPosition + (maxTiltedReelMovement * movementProgress)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 中间胶卷X位置
|
|
||||||
private var middleReelXPosition: CGFloat {
|
|
||||||
let startPosition: CGFloat = deviceWidth * 0.3
|
|
||||||
return startPosition + (maxMiddleReelMovement * movementProgress)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移动进度(0-1)
|
|
||||||
private var movementProgress: CGFloat {
|
|
||||||
if animationProgress < constantSpeedEnd {
|
|
||||||
return animationProgress / constantSpeedEnd
|
|
||||||
} else {
|
} else {
|
||||||
return 1.0 // 6秒后停止移动
|
// 匀速阶段:1→2(线性运动)
|
||||||
|
return 1.0 + (animationProgress - 1.0).truncatingRemainder(dividingBy: 1.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 缩放与显示控制
|
// MARK: - 轨道位置计算(轮播图核心算法)
|
||||||
|
|
||||||
// 中间胶卷缩放
|
private var upperTrackPosition: CGFloat {
|
||||||
private var middleScale: CGFloat {
|
// 右向轮播:使用负偏移实现向右移动
|
||||||
guard animationProgress >= constantSpeedEnd else {
|
let rawPosition = -effectiveProgress * trackLength
|
||||||
return 1.0
|
// 当移动距离超过单组长度时,重置位置(无痕循环关键)
|
||||||
|
return rawPosition.truncatingRemainder(dividingBy: -trackLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
let scalePhaseProgress = (animationProgress - constantSpeedEnd) / (1.0 - constantSpeedEnd)
|
private var lowerTrackPosition: CGFloat {
|
||||||
return 1.0 + (finalScale - 1.0) * scalePhaseProgress
|
// 右向轮播:与上方胶卷错开3帧位置
|
||||||
|
let rawPosition = -effectiveProgress * trackLength + frameTotalWidth * 3
|
||||||
|
return rawPosition.truncatingRemainder(dividingBy: -trackLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 倾斜胶卷缩放
|
private var middleTrackPosition: CGFloat {
|
||||||
private var tiltedScale: CGFloat {
|
// 左向轮播:使用正偏移实现向左移动
|
||||||
guard animationProgress >= constantSpeedEnd, animationProgress < scaleStartEnd else {
|
let rawPosition = effectiveProgress * trackLength
|
||||||
return 1.0
|
return rawPosition.truncatingRemainder(dividingBy: trackLength)
|
||||||
}
|
|
||||||
|
|
||||||
let scalePhaseProgress = (animationProgress - constantSpeedEnd) / (scaleStartEnd - constantSpeedEnd)
|
|
||||||
return 1.0 + (finalScale * 0.6 - 1.0) * scalePhaseProgress
|
|
||||||
}
|
|
||||||
|
|
||||||
// 倾斜胶卷透明度
|
|
||||||
private var tiltedOpacity: Double {
|
|
||||||
guard animationProgress >= constantSpeedEnd, animationProgress < scaleStartEnd else {
|
|
||||||
return 0.8
|
|
||||||
}
|
|
||||||
|
|
||||||
let fadeProgress = (animationProgress - constantSpeedEnd) / (scaleStartEnd - constantSpeedEnd)
|
|
||||||
return 0.8 * (1.0 - fadeProgress)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 控制倾斜胶卷显示
|
|
||||||
private var showTiltedReels: Bool {
|
|
||||||
animationProgress < scaleStartEnd
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 胶卷组件
|
// MARK: - 胶卷轮播组件(轮播图核心视图)
|
||||||
|
|
||||||
struct FilmReelView: View {
|
struct FilmCarouselView: View {
|
||||||
let images: [String]
|
let images: [String]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
|
// 核心轮播技巧:显示两组相同帧,当第一组移出视野时第二组无缝衔接
|
||||||
|
ForEach(images.indices, id: \.self) { index in
|
||||||
|
FilmFrameView(imageName: images[index])
|
||||||
|
}
|
||||||
|
// 复制一组帧实现无痕循环
|
||||||
ForEach(images.indices, id: \.self) { index in
|
ForEach(images.indices, id: \.self) { index in
|
||||||
FilmFrameView(imageName: images[index])
|
FilmFrameView(imageName: images[index])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 限制显示区域,超出部分隐藏(轮播图必要设置)
|
||||||
|
.mask(
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Rectangle()
|
||||||
|
.frame(width: UIScreen.main.bounds.width * 1.2) // 稍宽于屏幕确保无空白
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - 胶卷帧组件
|
||||||
|
|
||||||
struct FilmFrameView: View {
|
struct FilmFrameView: View {
|
||||||
let imageName: String
|
let imageName: String
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
// 胶卷边框
|
|
||||||
RoundedRectangle(cornerRadius: 4)
|
RoundedRectangle(cornerRadius: 4)
|
||||||
.stroke(Color.gray, lineWidth: 2)
|
.stroke(Color.gray, lineWidth: 2)
|
||||||
.background(Color(red: 0.15, green: 0.15, blue: 0.15))
|
.background(Color(red: 0.15, green: 0.15, blue: 0.15))
|
||||||
|
|
||||||
// 帧内容
|
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(gradientColor)
|
.fill(gradientColor)
|
||||||
.cornerRadius(2)
|
.cornerRadius(2)
|
||||||
.padding(2)
|
.padding(2)
|
||||||
|
|
||||||
// 帧标识
|
|
||||||
Text(imageName)
|
Text(imageName)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
}
|
}
|
||||||
.frame(width: 90, height: 130)
|
.frame(width: 90, height: 130)
|
||||||
// 胶卷孔洞
|
.overlay(alignment: .leading) {
|
||||||
.overlay(
|
|
||||||
HStack {
|
|
||||||
VStack(spacing: 6) {
|
VStack(spacing: 6) {
|
||||||
ForEach(0..<6) { _ in
|
ForEach(0..<6) { _ in
|
||||||
Circle()
|
Circle()
|
||||||
@ -218,7 +166,9 @@ struct FilmFrameView: View {
|
|||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer()
|
.padding(.leading, -3)
|
||||||
|
}
|
||||||
|
.overlay(alignment: .trailing) {
|
||||||
VStack(spacing: 6) {
|
VStack(spacing: 6) {
|
||||||
ForEach(0..<6) { _ in
|
ForEach(0..<6) { _ in
|
||||||
Circle()
|
Circle()
|
||||||
@ -226,17 +176,20 @@ struct FilmFrameView: View {
|
|||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.padding(.trailing, -3)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var gradientColor: LinearGradient {
|
private var gradientColor: LinearGradient {
|
||||||
if imageName.hasPrefix("film1") {
|
switch imageName.prefix(5) {
|
||||||
return LinearGradient(gradient: Gradient(colors: [.blue, .indigo]), startPoint: .topLeading, endPoint: .bottomTrailing)
|
case "film1":
|
||||||
} else if imageName.hasPrefix("film2") {
|
return LinearGradient(colors: [.blue, .indigo], startPoint: .topLeading, endPoint: .bottomTrailing)
|
||||||
return LinearGradient(gradient: Gradient(colors: [.yellow, .orange]), startPoint: .topLeading, endPoint: .bottomTrailing)
|
case "film2":
|
||||||
} else {
|
return LinearGradient(colors: [.yellow, .orange], startPoint: .topLeading, endPoint: .bottomTrailing)
|
||||||
return LinearGradient(gradient: Gradient(colors: [.purple, .pink]), startPoint: .topLeading, endPoint: .bottomTrailing)
|
case "film3":
|
||||||
|
return LinearGradient(colors: [.purple, .pink], startPoint: .topLeading, endPoint: .bottomTrailing)
|
||||||
|
default:
|
||||||
|
return LinearGradient(colors: [.gray, .white], startPoint: .topLeading, endPoint: .bottomTrailing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -245,6 +198,7 @@ struct FilmFrameView: View {
|
|||||||
struct FilmAnimation_Previews: PreviewProvider {
|
struct FilmAnimation_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
FilmAnimation()
|
FilmAnimation()
|
||||||
|
.background(Color.black)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
246
wake/View/Blind/Box7.swift
Normal file
246
wake/View/Blind/Box7.swift
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
// 胶卷帧数据模型
|
||||||
|
struct FilmFrame: Identifiable {
|
||||||
|
let id = UUID()
|
||||||
|
let imageName: String
|
||||||
|
let systemImage: String
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动画控制器
|
||||||
|
class FilmAnimationController: NSObject, ObservableObject {
|
||||||
|
@Published var offset: CGFloat = 0
|
||||||
|
var currentSpeed: CGFloat = 0
|
||||||
|
var lastUpdateTime: CFTimeInterval = 0
|
||||||
|
var startTime: CFTimeInterval = 0
|
||||||
|
var isAccelerating = true
|
||||||
|
var displayLink: CADisplayLink?
|
||||||
|
|
||||||
|
// 配置参数 - 提高最大速度
|
||||||
|
let frames: [FilmFrame]
|
||||||
|
let rotationAngle: Double
|
||||||
|
let verticalOffset: CGFloat
|
||||||
|
let direction: Direction
|
||||||
|
let initialSpeed: CGFloat
|
||||||
|
let maxSpeed: CGFloat // 增大此值提高最终速度
|
||||||
|
let accelerationDuration: Double
|
||||||
|
let frameWidth: CGFloat
|
||||||
|
let spacing: CGFloat
|
||||||
|
let frameCycleWidth: CGFloat
|
||||||
|
let isMiddleReel: Bool
|
||||||
|
|
||||||
|
// 运动方向枚举
|
||||||
|
enum Direction {
|
||||||
|
case left
|
||||||
|
case right
|
||||||
|
|
||||||
|
var multiplier: CGFloat {
|
||||||
|
switch self {
|
||||||
|
case .left: return -1
|
||||||
|
case .right: return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化 - 调整最大速度参数
|
||||||
|
init(frames: [FilmFrame], rotationAngle: Double, verticalOffset: CGFloat, direction: Direction,
|
||||||
|
isMiddleReel: Bool = false,
|
||||||
|
initialSpeed: CGFloat = 100, // 提高初始速度
|
||||||
|
maxSpeed: CGFloat = 600, // 显著提高最大速度(原400)
|
||||||
|
accelerationDuration: Double = 4, // 略微缩短加速时间
|
||||||
|
frameWidth: CGFloat = 100, spacing: CGFloat = 10) {
|
||||||
|
self.frames = frames
|
||||||
|
self.rotationAngle = rotationAngle
|
||||||
|
self.verticalOffset = verticalOffset
|
||||||
|
self.direction = direction
|
||||||
|
self.initialSpeed = initialSpeed
|
||||||
|
self.maxSpeed = maxSpeed
|
||||||
|
self.accelerationDuration = accelerationDuration
|
||||||
|
self.frameWidth = frameWidth
|
||||||
|
self.spacing = spacing
|
||||||
|
self.isMiddleReel = isMiddleReel
|
||||||
|
|
||||||
|
let baseFramesCount = CGFloat(frames.count)
|
||||||
|
let adjustedFramesCount = isMiddleReel ? baseFramesCount * 1.5 : baseFramesCount
|
||||||
|
self.frameCycleWidth = adjustedFramesCount * (frameWidth + spacing)
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动动画
|
||||||
|
func startAnimation() {
|
||||||
|
offset = -frameCycleWidth / (isMiddleReel ? 1.5 : 1)
|
||||||
|
currentSpeed = initialSpeed
|
||||||
|
startTime = CACurrentMediaTime()
|
||||||
|
lastUpdateTime = CACurrentMediaTime()
|
||||||
|
displayLink = CADisplayLink(target: self, selector: #selector(updateAnimation(_:)))
|
||||||
|
displayLink?.add(to: .main, forMode: .common)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止动画
|
||||||
|
func stopAnimation() {
|
||||||
|
displayLink?.invalidate()
|
||||||
|
displayLink = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每帧更新动画
|
||||||
|
@objc private func updateAnimation(_ displayLink: CADisplayLink) {
|
||||||
|
let currentTime = CACurrentMediaTime()
|
||||||
|
let deltaTime = currentTime - lastUpdateTime
|
||||||
|
lastUpdateTime = currentTime
|
||||||
|
|
||||||
|
// 处理加速
|
||||||
|
if isAccelerating {
|
||||||
|
let elapsedTime = currentTime - startTime
|
||||||
|
let progress = min(CGFloat(elapsedTime) / CGFloat(accelerationDuration), 1.0)
|
||||||
|
currentSpeed = initialSpeed + (maxSpeed - initialSpeed) * progress
|
||||||
|
|
||||||
|
if progress >= 1.0 {
|
||||||
|
isAccelerating = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算位移
|
||||||
|
let distance = currentSpeed * CGFloat(deltaTime) * direction.multiplier
|
||||||
|
offset += distance
|
||||||
|
|
||||||
|
// 循环重置
|
||||||
|
let cycleThreshold = isMiddleReel ? frameCycleWidth * 1.2 : frameCycleWidth * 2
|
||||||
|
|
||||||
|
if direction == .left {
|
||||||
|
while offset < -cycleThreshold {
|
||||||
|
offset += frameCycleWidth / (isMiddleReel ? 1.5 : 1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while offset > 0 {
|
||||||
|
offset -= frameCycleWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 胶卷视图
|
||||||
|
struct FilmReelView: View {
|
||||||
|
@StateObject private var animationController: FilmAnimationController
|
||||||
|
let zIndex: Double
|
||||||
|
|
||||||
|
init(frames: [FilmFrame], rotationAngle: Double, verticalOffset: CGFloat,
|
||||||
|
direction: FilmAnimationController.Direction, isMiddleReel: Bool = false, zIndex: Double = 0) {
|
||||||
|
_animationController = StateObject(wrappedValue: FilmAnimationController(
|
||||||
|
frames: frames,
|
||||||
|
rotationAngle: rotationAngle,
|
||||||
|
verticalOffset: verticalOffset,
|
||||||
|
direction: direction,
|
||||||
|
isMiddleReel: isMiddleReel
|
||||||
|
))
|
||||||
|
self.zIndex = zIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let containerWidth = animationController.frameCycleWidth * 4
|
||||||
|
|
||||||
|
ZStack {
|
||||||
|
Color.clear
|
||||||
|
.frame(width: containerWidth)
|
||||||
|
|
||||||
|
HStack(spacing: animationController.spacing) {
|
||||||
|
let repeatCount = animationController.isMiddleReel ? 4 : 3
|
||||||
|
ForEach(Array(repeating: animationController.frames, count: repeatCount).flatMap { $0 }) { frame in
|
||||||
|
ZStack {
|
||||||
|
RoundedRectangle(cornerRadius: 4)
|
||||||
|
.stroke(Color.black, lineWidth: 2)
|
||||||
|
.background(Color(white: 0.9, opacity: 0.7))
|
||||||
|
|
||||||
|
Group {
|
||||||
|
if let uiImage = UIImage(named: frame.imageName) {
|
||||||
|
Image(uiImage: uiImage)
|
||||||
|
.resizable()
|
||||||
|
} else {
|
||||||
|
Image(systemName: frame.systemImage)
|
||||||
|
.resizable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.padding(6)
|
||||||
|
.foregroundColor(animationController.isMiddleReel ? .red : .blue)
|
||||||
|
}
|
||||||
|
.frame(width: animationController.frameWidth, height: 150)
|
||||||
|
.shadow(radius: 3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.offset(x: animationController.offset, y: animationController.verticalOffset)
|
||||||
|
.onAppear {
|
||||||
|
animationController.startAnimation()
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
animationController.stopAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rotationEffect(.degrees(animationController.rotationAngle))
|
||||||
|
.zIndex(zIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主视图
|
||||||
|
struct Box7: View {
|
||||||
|
// 胶卷帧数据
|
||||||
|
private let filmFrames: [FilmFrame] = [
|
||||||
|
FilmFrame(imageName: "frame1", systemImage: "photo"),
|
||||||
|
FilmFrame(imageName: "frame2", systemImage: "camera"),
|
||||||
|
FilmFrame(imageName: "frame3", systemImage: "video"),
|
||||||
|
FilmFrame(imageName: "frame4", systemImage: "movie"),
|
||||||
|
FilmFrame(imageName: "frame5", systemImage: "film"),
|
||||||
|
FilmFrame(imageName: "frame6", systemImage: "photo.on.rectangle"),
|
||||||
|
FilmFrame(imageName: "frame7", systemImage: "photo.circle"),
|
||||||
|
FilmFrame(imageName: "frame8", systemImage: "film.frame")
|
||||||
|
]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack(alignment: .center) {
|
||||||
|
// 上边胶卷
|
||||||
|
FilmReelView(
|
||||||
|
frames: filmFrames,
|
||||||
|
rotationAngle: 6,
|
||||||
|
verticalOffset: -120,
|
||||||
|
direction: .right,
|
||||||
|
zIndex: 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// 中间胶卷
|
||||||
|
FilmReelView(
|
||||||
|
frames: filmFrames,
|
||||||
|
rotationAngle: 0,
|
||||||
|
verticalOffset: 0,
|
||||||
|
direction: .left,
|
||||||
|
isMiddleReel: true,
|
||||||
|
zIndex: 0
|
||||||
|
)
|
||||||
|
|
||||||
|
// 下边胶卷
|
||||||
|
FilmReelView(
|
||||||
|
frames: filmFrames,
|
||||||
|
rotationAngle: -6,
|
||||||
|
verticalOffset: 120,
|
||||||
|
direction: .right,
|
||||||
|
zIndex: 2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 60)
|
||||||
|
.padding(.vertical, 80)
|
||||||
|
.background(LinearGradient(
|
||||||
|
gradient: Gradient(colors: [.gray.opacity(0.1), .gray.opacity(0.2)]),
|
||||||
|
startPoint: .top,
|
||||||
|
endPoint: .bottom
|
||||||
|
))
|
||||||
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预览
|
||||||
|
struct Box7_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
Box7()
|
||||||
|
.previewDevice("iPhone 13")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,7 +19,7 @@ struct SplashView: View {
|
|||||||
)
|
)
|
||||||
.edgesIgnoringSafeArea(.all)
|
.edgesIgnoringSafeArea(.all)
|
||||||
VStack(spacing: 50) {
|
VStack(spacing: 50) {
|
||||||
FilmAnimation()
|
Box7()
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user