feat: 动效
This commit is contained in:
parent
949b371b49
commit
8fe4a8b8a5
@ -6,31 +6,54 @@ struct FilmFrame: Identifiable {
|
|||||||
let id = UUID()
|
let id = UUID()
|
||||||
let imageName: String
|
let imageName: String
|
||||||
let systemImage: 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 {
|
class FilmAnimationController: NSObject, ObservableObject {
|
||||||
@Published var offset: CGFloat = 0
|
@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 currentSpeed: CGFloat = 0
|
||||||
var lastUpdateTime: CFTimeInterval = 0
|
var lastUpdateTime: CFTimeInterval = 0
|
||||||
var startTime: CFTimeInterval = 0
|
var startTime: CFTimeInterval = 0
|
||||||
var isAccelerating = true
|
var uniformStartTime: CFTimeInterval = 0
|
||||||
|
var exitStartTime: CFTimeInterval = 0
|
||||||
var displayLink: CADisplayLink?
|
var displayLink: CADisplayLink?
|
||||||
|
var targetFrameIndex: Int // 目标帧索引
|
||||||
|
|
||||||
// 配置参数 - 提高最大速度
|
// 配置参数
|
||||||
let frames: [FilmFrame]
|
let frames: [FilmFrame]
|
||||||
let rotationAngle: Double
|
let verticalOffset: CGFloat // 初始垂直偏移
|
||||||
let verticalOffset: CGFloat
|
let exitDistance: CGFloat // 退出时的移动距离
|
||||||
let direction: Direction
|
let direction: Direction
|
||||||
let initialSpeed: CGFloat
|
let initialSpeed: CGFloat
|
||||||
let maxSpeed: CGFloat // 增大此值提高最终速度
|
let maxSpeed: CGFloat
|
||||||
let accelerationDuration: Double
|
let accelerationDuration: Double
|
||||||
let frameWidth: CGFloat
|
let frameWidth: CGFloat
|
||||||
let spacing: CGFloat
|
let spacing: CGFloat
|
||||||
let frameCycleWidth: CGFloat
|
let frameCycleWidth: CGFloat
|
||||||
let isMiddleReel: Bool
|
let isMiddleReel: Bool
|
||||||
|
weak var coordinator: AnimationCoordinator? // 弱引用协调器
|
||||||
|
|
||||||
// 运动方向枚举
|
|
||||||
enum Direction {
|
enum Direction {
|
||||||
case left
|
case left
|
||||||
case right
|
case right
|
||||||
@ -43,16 +66,17 @@ class FilmAnimationController: NSObject, ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化 - 调整最大速度参数
|
init(frames: [FilmFrame], rotationAngle: Double, verticalOffset: CGFloat, exitDistance: CGFloat,
|
||||||
init(frames: [FilmFrame], rotationAngle: Double, verticalOffset: CGFloat, direction: Direction,
|
direction: Direction, coordinator: AnimationCoordinator,
|
||||||
isMiddleReel: Bool = false,
|
isMiddleReel: Bool = false,
|
||||||
initialSpeed: CGFloat = 100, // 提高初始速度
|
initialSpeed: CGFloat = 100,
|
||||||
maxSpeed: CGFloat = 600, // 显著提高最大速度(原400)
|
maxSpeed: CGFloat = 600,
|
||||||
accelerationDuration: Double = 4, // 略微缩短加速时间
|
accelerationDuration: Double = 4,
|
||||||
frameWidth: CGFloat = 100, spacing: CGFloat = 10) {
|
frameWidth: CGFloat = 100, spacing: CGFloat = 10) {
|
||||||
self.frames = frames
|
self.frames = frames
|
||||||
self.rotationAngle = rotationAngle
|
self.originalRotation = rotationAngle
|
||||||
self.verticalOffset = verticalOffset
|
self.verticalOffset = verticalOffset
|
||||||
|
self.exitDistance = exitDistance
|
||||||
self.direction = direction
|
self.direction = direction
|
||||||
self.initialSpeed = initialSpeed
|
self.initialSpeed = initialSpeed
|
||||||
self.maxSpeed = maxSpeed
|
self.maxSpeed = maxSpeed
|
||||||
@ -60,17 +84,32 @@ class FilmAnimationController: NSObject, ObservableObject {
|
|||||||
self.frameWidth = frameWidth
|
self.frameWidth = frameWidth
|
||||||
self.spacing = spacing
|
self.spacing = spacing
|
||||||
self.isMiddleReel = isMiddleReel
|
self.isMiddleReel = isMiddleReel
|
||||||
|
self.coordinator = coordinator
|
||||||
|
self.reelIdentifier = UUID()
|
||||||
|
|
||||||
|
// 设置目标帧为最后一帧
|
||||||
|
self.targetFrameIndex = frames.count - 1
|
||||||
|
|
||||||
let baseFramesCount = CGFloat(frames.count)
|
let baseFramesCount = CGFloat(frames.count)
|
||||||
let adjustedFramesCount = isMiddleReel ? baseFramesCount * 1.5 : baseFramesCount
|
let adjustedFramesCount = isMiddleReel ? baseFramesCount * 1.5 : baseFramesCount
|
||||||
self.frameCycleWidth = adjustedFramesCount * (frameWidth + spacing)
|
self.frameCycleWidth = adjustedFramesCount * (frameWidth + spacing)
|
||||||
|
|
||||||
super.init()
|
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() {
|
func startAnimation() {
|
||||||
offset = -frameCycleWidth / (isMiddleReel ? 1.5 : 1)
|
offset.x = -frameCycleWidth / (isMiddleReel ? 1.5 : 1)
|
||||||
|
offset.y = verticalOffset
|
||||||
currentSpeed = initialSpeed
|
currentSpeed = initialSpeed
|
||||||
startTime = CACurrentMediaTime()
|
startTime = CACurrentMediaTime()
|
||||||
lastUpdateTime = CACurrentMediaTime()
|
lastUpdateTime = CACurrentMediaTime()
|
||||||
@ -78,43 +117,89 @@ class FilmAnimationController: NSObject, ObservableObject {
|
|||||||
displayLink?.add(to: .main, forMode: .common)
|
displayLink?.add(to: .main, forMode: .common)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止动画
|
|
||||||
func stopAnimation() {
|
func stopAnimation() {
|
||||||
displayLink?.invalidate()
|
displayLink?.invalidate()
|
||||||
displayLink = nil
|
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) {
|
@objc private func updateAnimation(_ displayLink: CADisplayLink) {
|
||||||
let currentTime = CACurrentMediaTime()
|
let currentTime = CACurrentMediaTime()
|
||||||
let deltaTime = currentTime - lastUpdateTime
|
let deltaTime = currentTime - lastUpdateTime
|
||||||
lastUpdateTime = currentTime
|
lastUpdateTime = currentTime
|
||||||
|
|
||||||
// 处理加速
|
guard isPresented else { return }
|
||||||
if isAccelerating {
|
|
||||||
|
switch phase {
|
||||||
|
case .accelerating:
|
||||||
let elapsedTime = currentTime - startTime
|
let elapsedTime = currentTime - startTime
|
||||||
let progress = min(CGFloat(elapsedTime) / CGFloat(accelerationDuration), 1.0)
|
let progress = min(CGFloat(elapsedTime) / CGFloat(accelerationDuration), 1.0)
|
||||||
currentSpeed = initialSpeed + (maxSpeed - initialSpeed) * progress
|
currentSpeed = initialSpeed + (maxSpeed - initialSpeed) * progress
|
||||||
|
|
||||||
if progress >= 1.0 {
|
if progress >= 1.0 {
|
||||||
isAccelerating = false
|
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 distance = currentSpeed * CGFloat(deltaTime) * direction.multiplier
|
|
||||||
offset += distance
|
|
||||||
|
|
||||||
// 循环重置
|
|
||||||
let cycleThreshold = isMiddleReel ? frameCycleWidth * 1.2 : frameCycleWidth * 2
|
let cycleThreshold = isMiddleReel ? frameCycleWidth * 1.2 : frameCycleWidth * 2
|
||||||
|
|
||||||
if direction == .left {
|
if direction == .left {
|
||||||
while offset < -cycleThreshold {
|
while offset.x < -cycleThreshold {
|
||||||
offset += frameCycleWidth / (isMiddleReel ? 1.5 : 1)
|
offset.x += frameCycleWidth / (isMiddleReel ? 1.5 : 1)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
while offset > 0 {
|
while offset.x > 0 {
|
||||||
offset -= frameCycleWidth
|
offset.x -= frameCycleWidth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -125,115 +210,262 @@ struct FilmReelView: View {
|
|||||||
@StateObject private var animationController: FilmAnimationController
|
@StateObject private var animationController: FilmAnimationController
|
||||||
let zIndex: Double
|
let zIndex: Double
|
||||||
|
|
||||||
init(frames: [FilmFrame], rotationAngle: Double, verticalOffset: CGFloat,
|
init(frames: [FilmFrame], rotationAngle: Double, verticalOffset: CGFloat, exitDistance: CGFloat,
|
||||||
direction: FilmAnimationController.Direction, isMiddleReel: Bool = false, zIndex: Double = 0) {
|
direction: FilmAnimationController.Direction, coordinator: AnimationCoordinator,
|
||||||
|
isMiddleReel: Bool = false, zIndex: Double = 0) {
|
||||||
_animationController = StateObject(wrappedValue: FilmAnimationController(
|
_animationController = StateObject(wrappedValue: FilmAnimationController(
|
||||||
frames: frames,
|
frames: frames,
|
||||||
rotationAngle: rotationAngle,
|
rotationAngle: rotationAngle,
|
||||||
verticalOffset: verticalOffset,
|
verticalOffset: verticalOffset,
|
||||||
|
exitDistance: exitDistance,
|
||||||
direction: direction,
|
direction: direction,
|
||||||
|
coordinator: coordinator,
|
||||||
isMiddleReel: isMiddleReel
|
isMiddleReel: isMiddleReel
|
||||||
))
|
))
|
||||||
self.zIndex = zIndex
|
self.zIndex = zIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let containerWidth = animationController.frameCycleWidth * 4
|
if animationController.isPresented {
|
||||||
|
let containerWidth = animationController.frameCycleWidth * 4
|
||||||
|
|
||||||
ZStack {
|
ZStack {
|
||||||
Color.clear
|
HStack(spacing: animationController.spacing) {
|
||||||
.frame(width: containerWidth)
|
let repeatCount = animationController.isMiddleReel ? 4 : 3
|
||||||
|
ForEach(Array(repeating: animationController.frames, count: repeatCount).flatMap { $0 }) { frame in
|
||||||
HStack(spacing: animationController.spacing) {
|
filmFrameView(frame: frame)
|
||||||
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.x, y: animationController.offset.y)
|
||||||
}
|
}
|
||||||
.offset(x: animationController.offset, y: animationController.verticalOffset)
|
.frame(width: containerWidth)
|
||||||
|
.rotationEffect(.degrees(animationController.originalRotation))
|
||||||
|
.scaleEffect(animationController.scale)
|
||||||
|
.opacity(animationController.opacity)
|
||||||
|
.zIndex(zIndex)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
animationController.startAnimation()
|
animationController.startAnimation()
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
animationController.stopAnimation()
|
animationController.stopAnimation()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
}
|
}
|
||||||
.rotationEffect(.degrees(animationController.rotationAngle))
|
}
|
||||||
.zIndex(zIndex)
|
|
||||||
|
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 {
|
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] = [
|
private let filmFrames: [FilmFrame] = [
|
||||||
FilmFrame(imageName: "frame1", systemImage: "photo"),
|
FilmFrame(imageName: "frame1", systemImage: "photo", svgName: "IP1"),
|
||||||
FilmFrame(imageName: "frame2", systemImage: "camera"),
|
FilmFrame(imageName: "frame2", systemImage: "camera", svgName: "IP2"),
|
||||||
FilmFrame(imageName: "frame3", systemImage: "video"),
|
FilmFrame(imageName: "frame3", systemImage: "video", svgName: "IP3"),
|
||||||
FilmFrame(imageName: "frame4", systemImage: "movie"),
|
FilmFrame(imageName: "frame4", systemImage: "movie", svgName: "IP4"),
|
||||||
FilmFrame(imageName: "frame5", systemImage: "film"),
|
FilmFrame(imageName: "frame5", systemImage: "film", svgName: "IP5"),
|
||||||
FilmFrame(imageName: "frame6", systemImage: "photo.on.rectangle"),
|
FilmFrame(imageName: "frame6", systemImage: "photo.on.rectangle", svgName: "IP6"),
|
||||||
FilmFrame(imageName: "frame7", systemImage: "photo.circle"),
|
FilmFrame(imageName: "frame7", systemImage: "photo.circle", svgName: "IP7"),
|
||||||
FilmFrame(imageName: "frame8", systemImage: "film.frame")
|
FilmFrame(imageName: "frame8", systemImage: "film.frame", svgName: "IP") // 最后一帧使用"IP" SVG
|
||||||
]
|
]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .center) {
|
ZStack(alignment: .center) {
|
||||||
// 上边胶卷
|
// 背景
|
||||||
|
LinearGradient(
|
||||||
|
gradient: Gradient(colors: [.gray.opacity(0.1), .gray.opacity(0.2)]),
|
||||||
|
startPoint: .top,
|
||||||
|
endPoint: .bottom
|
||||||
|
)
|
||||||
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
|
||||||
|
// 所有胶卷
|
||||||
|
// 上边胶卷 - 向上移动退出
|
||||||
FilmReelView(
|
FilmReelView(
|
||||||
frames: filmFrames,
|
frames: filmFrames,
|
||||||
rotationAngle: 6,
|
rotationAngle: 6,
|
||||||
verticalOffset: -120,
|
verticalOffset: -120,
|
||||||
|
exitDistance: -250,
|
||||||
direction: .right,
|
direction: .right,
|
||||||
zIndex: 1
|
coordinator: coordinator,
|
||||||
|
zIndex: 2
|
||||||
)
|
)
|
||||||
|
|
||||||
// 中间胶卷
|
// 中间胶卷 - 直接消失
|
||||||
FilmReelView(
|
FilmReelView(
|
||||||
frames: filmFrames,
|
frames: filmFrames,
|
||||||
rotationAngle: 0,
|
rotationAngle: 0,
|
||||||
verticalOffset: 0,
|
verticalOffset: 0,
|
||||||
|
exitDistance: 0,
|
||||||
direction: .left,
|
direction: .left,
|
||||||
|
coordinator: coordinator,
|
||||||
isMiddleReel: true,
|
isMiddleReel: true,
|
||||||
zIndex: 0
|
zIndex: 1
|
||||||
)
|
)
|
||||||
|
|
||||||
// 下边胶卷
|
// 下边胶卷 - 向下移动退出
|
||||||
FilmReelView(
|
FilmReelView(
|
||||||
frames: filmFrames,
|
frames: filmFrames,
|
||||||
rotationAngle: -6,
|
rotationAngle: -6,
|
||||||
verticalOffset: 120,
|
verticalOffset: 120,
|
||||||
|
exitDistance: 250,
|
||||||
direction: .right,
|
direction: .right,
|
||||||
|
coordinator: coordinator,
|
||||||
zIndex: 2
|
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(.horizontal, 60)
|
||||||
.padding(.vertical, 80)
|
.padding(.vertical, 80)
|
||||||
.background(LinearGradient(
|
|
||||||
gradient: Gradient(colors: [.gray.opacity(0.1), .gray.opacity(0.2)]),
|
|
||||||
startPoint: .top,
|
|
||||||
endPoint: .bottom
|
|
||||||
))
|
|
||||||
.edgesIgnoringSafeArea(.all)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user