Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fe4a8b8a5 | ||
|
|
949b371b49 |
@ -3,240 +3,193 @@ import SwiftUI
|
||||
struct FilmAnimation: 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
|
||||
@State private var animationProgress: CGFloat = 0.0
|
||||
@State private var hasAccelerated: Bool = false // 标记是否完成第一遍加速
|
||||
|
||||
// 胶卷数据
|
||||
private let reelImages: [[String]] = [
|
||||
(0..<300).map { "film1-\($0+1)" }, // 上方胶卷
|
||||
(0..<350).map { "film2-\($0+1)" }, // 中间胶卷
|
||||
(0..<300).map { "film3-\($0+1)" } // 下方胶卷
|
||||
]
|
||||
// 胶卷数据(16帧)
|
||||
private let reelFrames: Int = 16
|
||||
private var reel1: [String] { (0..<reelFrames).map { "film1-\($0+1)" } }
|
||||
private var reel2: [String] { (0..<reelFrames).map { "film2-\($0+1)" } }
|
||||
private var reel3: [String] { (0..<reelFrames).map { "film3-\($0+1)" } }
|
||||
|
||||
// 胶卷参数
|
||||
// 胶卷参数(轮播图关键参数)
|
||||
private let frameWidth: CGFloat = 90
|
||||
private let frameHeight: CGFloat = 130
|
||||
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 constantSpeedDuration: Double = 1.0 // 5-6s匀速
|
||||
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 let accelerationDuration: Double = 3.0 // 加速时间
|
||||
private let cycleDuration: Double = 2.0 // 匀速循环周期
|
||||
|
||||
// 各阶段进度阈值
|
||||
private var accelerationEnd: CGFloat { accelerationDuration / totalDuration }
|
||||
private var constantSpeedEnd: CGFloat { (accelerationDuration + constantSpeedDuration) / totalDuration }
|
||||
private var scaleStartEnd: CGFloat {
|
||||
(accelerationDuration + constantSpeedDuration + scaleStartDuration) / totalDuration
|
||||
}
|
||||
// 布局与运动参数
|
||||
private let upperTilt: Double = -10 // 上卷左高右低
|
||||
private let lowerTilt: Double = 10 // 下卷左低右高
|
||||
private let verticalSpacing: CGFloat = 200
|
||||
|
||||
// 布局与运动参数(核心:对称倾斜角度)
|
||||
private let tiltAngle: Double = 10 // 基础倾斜角度
|
||||
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 // 中间胶卷最大移动距离
|
||||
// 轮播关键参数(轨道长度 = 单组帧总宽度)
|
||||
private var trackLength: CGFloat { CGFloat(reelFrames) * frameTotalWidth }
|
||||
|
||||
var body: some View {
|
||||
// 固定背景
|
||||
Color(red: 0.08, green: 0.08, blue: 0.08)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.overlay(
|
||||
ZStack {
|
||||
// 上方倾斜胶卷(左高右低,向右移动)
|
||||
if showTiltedReels {
|
||||
FilmReelView(images: reelImages[0])
|
||||
.rotationEffect(Angle(degrees: upperTilt)) // 左高右低
|
||||
.offset(x: upperReelXPosition, y: -verticalSpacing/2)
|
||||
.scaleEffect(tiltedScale)
|
||||
.opacity(tiltedOpacity)
|
||||
.zIndex(1)
|
||||
}
|
||||
|
||||
// 下方倾斜胶卷(左低右高,向右移动)
|
||||
if showTiltedReels {
|
||||
FilmReelView(images: reelImages[2])
|
||||
.rotationEffect(Angle(degrees: lowerTilt)) // 左低右高
|
||||
.offset(x: lowerReelXPosition, y: verticalSpacing/2)
|
||||
.scaleEffect(tiltedScale)
|
||||
.opacity(tiltedOpacity)
|
||||
.zIndex(1)
|
||||
}
|
||||
|
||||
// 中间胶卷(垂直,向左移动)
|
||||
FilmReelView(images: reelImages[1])
|
||||
.offset(x: middleReelXPosition, y: 0)
|
||||
.scaleEffect(middleScale)
|
||||
.opacity(1.0)
|
||||
.zIndex(2)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
)
|
||||
.onAppear {
|
||||
startAnimation()
|
||||
}
|
||||
ZStack {
|
||||
// 上方胶卷(右向轮播)
|
||||
FilmCarouselView(images: reel1)
|
||||
.rotationEffect(Angle(degrees: upperTilt))
|
||||
.offset(x: upperTrackPosition, y: -verticalSpacing/2)
|
||||
.zIndex(1)
|
||||
|
||||
// 下方胶卷(右向轮播)
|
||||
FilmCarouselView(images: reel3)
|
||||
.rotationEffect(Angle(degrees: lowerTilt))
|
||||
.offset(x: lowerTrackPosition, y: verticalSpacing/2)
|
||||
.zIndex(1)
|
||||
|
||||
// 中间胶卷(左向轮播)
|
||||
FilmCarouselView(images: reel2)
|
||||
.offset(x: middleTrackPosition, y: 0)
|
||||
.zIndex(2)
|
||||
}
|
||||
.onAppear {
|
||||
startCarouselAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 动画逻辑
|
||||
// MARK: - 轮播动画控制(核心轮播逻辑)
|
||||
|
||||
private func startAnimation() {
|
||||
guard !isAnimating && !animationComplete else { return }
|
||||
isAnimating = true
|
||||
|
||||
withAnimation(Animation.easeInOut(duration: totalDuration)) {
|
||||
private func startCarouselAnimation() {
|
||||
// 第一阶段:加速动画
|
||||
withAnimation(Animation.linear(duration: accelerationDuration)) {
|
||||
animationProgress = 1.0
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + totalDuration) {
|
||||
isAnimating = false
|
||||
animationComplete = true
|
||||
// 加速完成后进入匀速循环
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + accelerationDuration) {
|
||||
hasAccelerated = true
|
||||
// 匀速循环动画
|
||||
withAnimation(Animation.linear(duration: cycleDuration).repeatForever(autoreverses: false)) {
|
||||
animationProgress = 2.0 // 从1.0到2.0实现一次循环
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 位置计算(确保向右移动)
|
||||
// MARK: - 速度计算(加速到匀速过渡)
|
||||
|
||||
// 上方倾斜胶卷X位置
|
||||
private var upperReelXPosition: CGFloat {
|
||||
let startPosition: CGFloat = -deviceWidth * 1.2 // 左侧屏幕外起始
|
||||
return startPosition + (maxTiltedReelMovement * movementProgress)
|
||||
}
|
||||
|
||||
// 下方倾斜胶卷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
|
||||
private var effectiveProgress: CGFloat {
|
||||
if !hasAccelerated {
|
||||
// 加速阶段:0→1(三次方曲线加速)
|
||||
return animationProgress * animationProgress * animationProgress
|
||||
} else {
|
||||
return 1.0 // 6秒后停止移动
|
||||
// 匀速阶段:1→2(线性运动)
|
||||
return 1.0 + (animationProgress - 1.0).truncatingRemainder(dividingBy: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 缩放与显示控制
|
||||
// MARK: - 轨道位置计算(轮播图核心算法)
|
||||
|
||||
// 中间胶卷缩放
|
||||
private var middleScale: CGFloat {
|
||||
guard animationProgress >= constantSpeedEnd else {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
let scalePhaseProgress = (animationProgress - constantSpeedEnd) / (1.0 - constantSpeedEnd)
|
||||
return 1.0 + (finalScale - 1.0) * scalePhaseProgress
|
||||
private var upperTrackPosition: CGFloat {
|
||||
// 右向轮播:使用负偏移实现向右移动
|
||||
let rawPosition = -effectiveProgress * trackLength
|
||||
// 当移动距离超过单组长度时,重置位置(无痕循环关键)
|
||||
return rawPosition.truncatingRemainder(dividingBy: -trackLength)
|
||||
}
|
||||
|
||||
// 倾斜胶卷缩放
|
||||
private var tiltedScale: CGFloat {
|
||||
guard animationProgress >= constantSpeedEnd, animationProgress < scaleStartEnd else {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
let scalePhaseProgress = (animationProgress - constantSpeedEnd) / (scaleStartEnd - constantSpeedEnd)
|
||||
return 1.0 + (finalScale * 0.6 - 1.0) * scalePhaseProgress
|
||||
private var lowerTrackPosition: CGFloat {
|
||||
// 右向轮播:与上方胶卷错开3帧位置
|
||||
let rawPosition = -effectiveProgress * trackLength + frameTotalWidth * 3
|
||||
return rawPosition.truncatingRemainder(dividingBy: -trackLength)
|
||||
}
|
||||
|
||||
// 倾斜胶卷透明度
|
||||
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
|
||||
private var middleTrackPosition: CGFloat {
|
||||
// 左向轮播:使用正偏移实现向左移动
|
||||
let rawPosition = effectiveProgress * trackLength
|
||||
return rawPosition.truncatingRemainder(dividingBy: trackLength)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 胶卷组件
|
||||
// MARK: - 胶卷轮播组件(轮播图核心视图)
|
||||
|
||||
struct FilmReelView: View {
|
||||
struct FilmCarouselView: View {
|
||||
let images: [String]
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
// 核心轮播技巧:显示两组相同帧,当第一组移出视野时第二组无缝衔接
|
||||
ForEach(images.indices, id: \.self) { index in
|
||||
FilmFrameView(imageName: images[index])
|
||||
}
|
||||
// 复制一组帧实现无痕循环
|
||||
ForEach(images.indices, id: \.self) { index in
|
||||
FilmFrameView(imageName: images[index])
|
||||
}
|
||||
}
|
||||
// 限制显示区域,超出部分隐藏(轮播图必要设置)
|
||||
.mask(
|
||||
HStack {
|
||||
Spacer()
|
||||
Rectangle()
|
||||
.frame(width: UIScreen.main.bounds.width * 1.2) // 稍宽于屏幕确保无空白
|
||||
Spacer()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 胶卷帧组件
|
||||
|
||||
struct FilmFrameView: 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)
|
||||
}
|
||||
.overlay(alignment: .leading) {
|
||||
VStack(spacing: 6) {
|
||||
ForEach(0..<6) { _ in
|
||||
Circle()
|
||||
.frame(width: 6, height: 6)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
)
|
||||
.padding(.leading, -3)
|
||||
}
|
||||
.overlay(alignment: .trailing) {
|
||||
VStack(spacing: 6) {
|
||||
ForEach(0..<6) { _ in
|
||||
Circle()
|
||||
.frame(width: 6, height: 6)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
.padding(.trailing, -3)
|
||||
}
|
||||
}
|
||||
|
||||
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: [.purple, .pink]), startPoint: .topLeading, endPoint: .bottomTrailing)
|
||||
switch imageName.prefix(5) {
|
||||
case "film1":
|
||||
return LinearGradient(colors: [.blue, .indigo], startPoint: .topLeading, endPoint: .bottomTrailing)
|
||||
case "film2":
|
||||
return LinearGradient(colors: [.yellow, .orange], 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 {
|
||||
static var previews: some View {
|
||||
FilmAnimation()
|
||||
.background(Color.black)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
478
wake/View/Blind/Box7.swift
Normal file
478
wake/View/Blind/Box7.swift
Normal file
@ -0,0 +1,478 @@
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
// 胶卷帧数据模型
|
||||
struct FilmFrame: Identifiable {
|
||||
let id = UUID()
|
||||
let imageName: String
|
||||
let systemImage: String
|
||||
let svgName: String // 用于展示的SVG名称
|
||||
}
|
||||
|
||||
// 动画阶段枚举
|
||||
enum AnimationPhase {
|
||||
case accelerating // 加速阶段
|
||||
case uniform // 匀速阶段
|
||||
case exiting // 退出阶段(上下胶卷外移,中间胶卷消失)
|
||||
case stationary // 静止阶段
|
||||
}
|
||||
|
||||
// 主动画协调器 - 用于同步所有胶卷动画
|
||||
class AnimationCoordinator: ObservableObject {
|
||||
@Published var shouldStartExiting = false // 控制所有胶卷是否开始退出
|
||||
}
|
||||
|
||||
// 动画控制器
|
||||
class FilmAnimationController: NSObject, ObservableObject {
|
||||
@Published var offset: CGPoint = .zero
|
||||
@Published var scale: CGFloat = 1.0
|
||||
@Published var opacity: Double = 1.0
|
||||
@Published var isPresented: Bool = true
|
||||
@Published var phase: AnimationPhase = .accelerating
|
||||
let originalRotation: Double
|
||||
let reelIdentifier: UUID // 用于标识不同胶卷
|
||||
|
||||
var currentSpeed: CGFloat = 0
|
||||
var lastUpdateTime: CFTimeInterval = 0
|
||||
var startTime: CFTimeInterval = 0
|
||||
var uniformStartTime: CFTimeInterval = 0
|
||||
var exitStartTime: CFTimeInterval = 0
|
||||
var displayLink: CADisplayLink?
|
||||
var targetFrameIndex: Int // 目标帧索引
|
||||
|
||||
// 配置参数
|
||||
let frames: [FilmFrame]
|
||||
let verticalOffset: CGFloat // 初始垂直偏移
|
||||
let exitDistance: 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
|
||||
weak var coordinator: AnimationCoordinator? // 弱引用协调器
|
||||
|
||||
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, exitDistance: CGFloat,
|
||||
direction: Direction, coordinator: AnimationCoordinator,
|
||||
isMiddleReel: Bool = false,
|
||||
initialSpeed: CGFloat = 100,
|
||||
maxSpeed: CGFloat = 600,
|
||||
accelerationDuration: Double = 4,
|
||||
frameWidth: CGFloat = 100, spacing: CGFloat = 10) {
|
||||
self.frames = frames
|
||||
self.originalRotation = rotationAngle
|
||||
self.verticalOffset = verticalOffset
|
||||
self.exitDistance = exitDistance
|
||||
self.direction = direction
|
||||
self.initialSpeed = initialSpeed
|
||||
self.maxSpeed = maxSpeed
|
||||
self.accelerationDuration = accelerationDuration
|
||||
self.frameWidth = frameWidth
|
||||
self.spacing = spacing
|
||||
self.isMiddleReel = isMiddleReel
|
||||
self.coordinator = coordinator
|
||||
self.reelIdentifier = UUID()
|
||||
|
||||
// 设置目标帧为最后一帧
|
||||
self.targetFrameIndex = frames.count - 1
|
||||
|
||||
let baseFramesCount = CGFloat(frames.count)
|
||||
let adjustedFramesCount = isMiddleReel ? baseFramesCount * 1.5 : baseFramesCount
|
||||
self.frameCycleWidth = adjustedFramesCount * (frameWidth + spacing)
|
||||
|
||||
super.init()
|
||||
|
||||
// 监听协调器的退出信号
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(startExitingWhenTriggered), name: NSNotification.Name("StartExiting"), object: nil)
|
||||
}
|
||||
|
||||
// 当协调器发出信号时开始退出
|
||||
@objc private func startExitingWhenTriggered() {
|
||||
if self.phase == .uniform {
|
||||
startExitAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
func startAnimation() {
|
||||
offset.x = -frameCycleWidth / (isMiddleReel ? 1.5 : 1)
|
||||
offset.y = verticalOffset
|
||||
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
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
// 开始退出动画
|
||||
func startExitAnimation() {
|
||||
phase = .exiting
|
||||
exitStartTime = CACurrentMediaTime()
|
||||
|
||||
if isMiddleReel {
|
||||
// 中间胶卷:直接消失
|
||||
withAnimation(.easeIn(duration: 0.6)) {
|
||||
self.opacity = 0.0
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
|
||||
self.isPresented = false
|
||||
}
|
||||
} else {
|
||||
// 上下胶卷:向外移动并淡出
|
||||
withAnimation(.easeInOut(duration: 1.2)) {
|
||||
self.offset.y = self.verticalOffset + self.exitDistance
|
||||
self.opacity = 0.0
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) {
|
||||
self.isPresented = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func updateAnimation(_ displayLink: CADisplayLink) {
|
||||
let currentTime = CACurrentMediaTime()
|
||||
let deltaTime = currentTime - lastUpdateTime
|
||||
lastUpdateTime = currentTime
|
||||
|
||||
guard isPresented else { return }
|
||||
|
||||
switch phase {
|
||||
case .accelerating:
|
||||
let elapsedTime = currentTime - startTime
|
||||
let progress = min(CGFloat(elapsedTime) / CGFloat(accelerationDuration), 1.0)
|
||||
currentSpeed = initialSpeed + (maxSpeed - initialSpeed) * progress
|
||||
|
||||
if progress >= 1.0 {
|
||||
phase = .uniform
|
||||
uniformStartTime = currentTime
|
||||
NotificationCenter.default.post(name: NSNotification.Name("ReelEnteredUniform"), object: reelIdentifier)
|
||||
}
|
||||
|
||||
let distance = currentSpeed * CGFloat(deltaTime) * direction.multiplier
|
||||
offset.x += distance
|
||||
resetOffset()
|
||||
|
||||
case .uniform:
|
||||
let uniformTimeElapsed = currentTime - uniformStartTime
|
||||
if uniformTimeElapsed >= 3.0 {
|
||||
if isMiddleReel {
|
||||
NotificationCenter.default.post(name: NSNotification.Name("AllReelsReadyToExit"), object: nil)
|
||||
}
|
||||
} else {
|
||||
let distance = currentSpeed * CGFloat(deltaTime) * direction.multiplier
|
||||
offset.x += distance
|
||||
resetOffset()
|
||||
}
|
||||
|
||||
case .exiting, .stationary:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func resetOffset() {
|
||||
let cycleThreshold = isMiddleReel ? frameCycleWidth * 1.2 : frameCycleWidth * 2
|
||||
|
||||
if direction == .left {
|
||||
while offset.x < -cycleThreshold {
|
||||
offset.x += frameCycleWidth / (isMiddleReel ? 1.5 : 1)
|
||||
}
|
||||
} else {
|
||||
while offset.x > 0 {
|
||||
offset.x -= frameCycleWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 胶卷视图
|
||||
struct FilmReelView: View {
|
||||
@StateObject private var animationController: FilmAnimationController
|
||||
let zIndex: Double
|
||||
|
||||
init(frames: [FilmFrame], rotationAngle: Double, verticalOffset: CGFloat, exitDistance: CGFloat,
|
||||
direction: FilmAnimationController.Direction, coordinator: AnimationCoordinator,
|
||||
isMiddleReel: Bool = false, zIndex: Double = 0) {
|
||||
_animationController = StateObject(wrappedValue: FilmAnimationController(
|
||||
frames: frames,
|
||||
rotationAngle: rotationAngle,
|
||||
verticalOffset: verticalOffset,
|
||||
exitDistance: exitDistance,
|
||||
direction: direction,
|
||||
coordinator: coordinator,
|
||||
isMiddleReel: isMiddleReel
|
||||
))
|
||||
self.zIndex = zIndex
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if animationController.isPresented {
|
||||
let containerWidth = animationController.frameCycleWidth * 4
|
||||
|
||||
ZStack {
|
||||
HStack(spacing: animationController.spacing) {
|
||||
let repeatCount = animationController.isMiddleReel ? 4 : 3
|
||||
ForEach(Array(repeating: animationController.frames, count: repeatCount).flatMap { $0 }) { frame in
|
||||
filmFrameView(frame: frame)
|
||||
}
|
||||
}
|
||||
.offset(x: animationController.offset.x, y: animationController.offset.y)
|
||||
}
|
||||
.frame(width: containerWidth)
|
||||
.rotationEffect(.degrees(animationController.originalRotation))
|
||||
.scaleEffect(animationController.scale)
|
||||
.opacity(animationController.opacity)
|
||||
.zIndex(zIndex)
|
||||
.onAppear {
|
||||
animationController.startAnimation()
|
||||
}
|
||||
.onDisappear {
|
||||
animationController.stopAnimation()
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
private func filmFrameView(frame: FilmFrame) -> some View {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 抖动效果修饰器
|
||||
struct ShakeEffect: GeometryEffect {
|
||||
var amount: CGFloat = 10
|
||||
var shakesPerUnit: CGFloat = 3
|
||||
var animatableData: CGFloat
|
||||
|
||||
func effectValue(size: CGSize) -> ProjectionTransform {
|
||||
ProjectionTransform(CGAffineTransform(translationX:
|
||||
amount * sin(animatableData * .pi * shakesPerUnit),
|
||||
y: 0))
|
||||
}
|
||||
}
|
||||
|
||||
// 主SVG视图 - 带抖动反转效果
|
||||
struct MainSVGView: View {
|
||||
@Binding var isVisible: Bool
|
||||
@State private var scale: CGFloat = 0.1
|
||||
@State private var opacity: Double = 0.0
|
||||
@State private var showFront: Bool = true
|
||||
@State private var shakeAmount: CGFloat = 0
|
||||
@State private var rotation: Double = 0
|
||||
let svgName: String // SVG资源名称
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if showFront {
|
||||
// 正面:缩略图风格的SVG
|
||||
SVGImage(svgName: "IP")
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.padding()
|
||||
.background(Color.white)
|
||||
.cornerRadius(12)
|
||||
.shadow(radius: 15)
|
||||
.rotation3DEffect(.degrees(rotation), axis: (x: 0, y: 1, z: 0))
|
||||
} else {
|
||||
// 反面:完整展示的SVG
|
||||
SVGImage(svgName: svgName)
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.padding()
|
||||
.background(Color.white)
|
||||
.cornerRadius(12)
|
||||
.shadow(radius: 15)
|
||||
.rotation3DEffect(.degrees(rotation), axis: (x: 0, y: 1, z: 0))
|
||||
}
|
||||
}
|
||||
.scaleEffect(scale)
|
||||
.opacity(opacity)
|
||||
.modifier(ShakeEffect(animatableData: shakeAmount))
|
||||
.onAppear {
|
||||
// 入场动画:由小变大并带有轻微抖动
|
||||
withAnimation(.easeOut(duration: 1.5).delay(0.3)) {
|
||||
self.scale = 1.0
|
||||
self.opacity = 1.0
|
||||
self.shakeAmount = 1 // 触发轻微抖动
|
||||
}
|
||||
|
||||
// 抖动结束后重置
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.8) {
|
||||
withAnimation(.linear(duration: 0.5)) {
|
||||
self.shakeAmount = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 延迟后自动翻转,带有抖动效果
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.3) {
|
||||
// 开始抖动
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
self.shakeAmount = 2 // 增强抖动
|
||||
}
|
||||
|
||||
// 抖动中开始反转
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
||||
withAnimation(.easeInOut(duration: 1.0)) {
|
||||
self.rotation = 180 // 完成翻转
|
||||
}
|
||||
|
||||
// 翻转完成后停止抖动
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
|
||||
withAnimation(.linear(duration: 0.5)) {
|
||||
self.shakeAmount = 0
|
||||
self.showFront = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
}
|
||||
|
||||
// 主视图
|
||||
struct Box7: View {
|
||||
@StateObject private var coordinator = AnimationCoordinator()
|
||||
@State private var showMainSVG = false
|
||||
@State private var readyReelsCount = 0
|
||||
private let totalReels = 3 // 总胶卷数量
|
||||
|
||||
// 胶卷帧数据 - 使用svgName替代fullImageName
|
||||
private let filmFrames: [FilmFrame] = [
|
||||
FilmFrame(imageName: "frame1", systemImage: "photo", svgName: "IP1"),
|
||||
FilmFrame(imageName: "frame2", systemImage: "camera", svgName: "IP2"),
|
||||
FilmFrame(imageName: "frame3", systemImage: "video", svgName: "IP3"),
|
||||
FilmFrame(imageName: "frame4", systemImage: "movie", svgName: "IP4"),
|
||||
FilmFrame(imageName: "frame5", systemImage: "film", svgName: "IP5"),
|
||||
FilmFrame(imageName: "frame6", systemImage: "photo.on.rectangle", svgName: "IP6"),
|
||||
FilmFrame(imageName: "frame7", systemImage: "photo.circle", svgName: "IP7"),
|
||||
FilmFrame(imageName: "frame8", systemImage: "film.frame", svgName: "IP") // 最后一帧使用"IP" SVG
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .center) {
|
||||
// 背景
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [.gray.opacity(0.1), .gray.opacity(0.2)]),
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
|
||||
// 所有胶卷
|
||||
// 上边胶卷 - 向上移动退出
|
||||
FilmReelView(
|
||||
frames: filmFrames,
|
||||
rotationAngle: 6,
|
||||
verticalOffset: -120,
|
||||
exitDistance: -250,
|
||||
direction: .right,
|
||||
coordinator: coordinator,
|
||||
zIndex: 2
|
||||
)
|
||||
|
||||
// 中间胶卷 - 直接消失
|
||||
FilmReelView(
|
||||
frames: filmFrames,
|
||||
rotationAngle: 0,
|
||||
verticalOffset: 0,
|
||||
exitDistance: 0,
|
||||
direction: .left,
|
||||
coordinator: coordinator,
|
||||
isMiddleReel: true,
|
||||
zIndex: 1
|
||||
)
|
||||
|
||||
// 下边胶卷 - 向下移动退出
|
||||
FilmReelView(
|
||||
frames: filmFrames,
|
||||
rotationAngle: -6,
|
||||
verticalOffset: 120,
|
||||
exitDistance: 250,
|
||||
direction: .right,
|
||||
coordinator: coordinator,
|
||||
zIndex: 2
|
||||
)
|
||||
|
||||
// 主SVG - 在胶卷退出后出现,使用SVGImage(svgName: "IP")
|
||||
if showMainSVG {
|
||||
MainSVGView(
|
||||
isVisible: $showMainSVG,
|
||||
svgName: filmFrames.last!.svgName // 传递"IP" SVG名称
|
||||
)
|
||||
.zIndex(10) // 确保在最上层
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
// 监听胶卷进入匀速阶段的通知
|
||||
NotificationCenter.default.addObserver(forName: NSNotification.Name("ReelEnteredUniform"), object: nil, queue: .main) { _ in
|
||||
readyReelsCount += 1
|
||||
if readyReelsCount >= totalReels {
|
||||
print("所有胶卷已准备就绪")
|
||||
}
|
||||
}
|
||||
|
||||
// 监听开始退出的通知
|
||||
NotificationCenter.default.addObserver(forName: NSNotification.Name("AllReelsReadyToExit"), object: nil, queue: .main) { _ in
|
||||
coordinator.shouldStartExiting = true
|
||||
NotificationCenter.default.post(name: NSNotification.Name("StartExiting"), object: nil)
|
||||
|
||||
// 胶卷开始退出后,准备显示主SVG
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
|
||||
showMainSVG = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
.padding(.horizontal, 60)
|
||||
.padding(.vertical, 80)
|
||||
}
|
||||
}
|
||||
|
||||
// 预览
|
||||
struct Box7_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Box7()
|
||||
.previewDevice("iPhone 13")
|
||||
}
|
||||
}
|
||||
@ -19,7 +19,7 @@ struct SplashView: View {
|
||||
)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
VStack(spacing: 50) {
|
||||
FilmAnimation()
|
||||
Box7()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user