diff --git a/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate b/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate index 4bcdc42..6dc6901 100644 Binary files a/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate and b/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/wake/View/Blind/Box.swift b/wake/View/Blind/Box.swift new file mode 100644 index 0000000..9da0c25 --- /dev/null +++ b/wake/View/Blind/Box.swift @@ -0,0 +1,301 @@ +import SwiftUI + +struct FilmStripView: View { + @State private var animate = false + // 使用SF Symbols名称数组 + private let symbolNames = [ + "photo.fill", "heart.fill", "star.fill", "bookmark.fill", + "flag.fill", "bell.fill", "tag.fill", "paperplane.fill" + ] + private let targetIndices = [2, 5, 3] // 每条胶片最终停止的位置 + + var body: some View { + ZStack { + Color.black.edgesIgnoringSafeArea(.all) + + // 三条胶片带 + FilmStrip( + symbols: symbolNames, + targetIndex: targetIndices[0], + offset: 0, + stripColor: .red + ) + .rotationEffect(.degrees(5)) + .zIndex(1) + + FilmStrip( + symbols: symbolNames, + targetIndex: targetIndices[1], + offset: 0.3, + stripColor: .blue + ) + .rotationEffect(.degrees(-3)) + .zIndex(2) + + FilmStrip( + symbols: symbolNames, + targetIndex: targetIndices[2], + offset: 0.6, + stripColor: .green + ) + .rotationEffect(.degrees(2)) + .zIndex(3) + } + .onAppear { + withAnimation( + .timingCurve(0.2, 0.1, 0.8, 0.9, duration: 4.0) + ) { + animate = true + } + } + } +} + +// 单个胶片带视图 +struct FilmStrip: View { + let symbols: [String] + let targetIndex: Int + let offset: Double + let stripColor: Color + @State private var animate = false + + var body: some View { + GeometryReader { geometry in + let itemWidth: CGFloat = 100 + let spacing: CGFloat = 8 + let totalWidth = itemWidth * CGFloat(symbols.count) + spacing * CGFloat(symbols.count - 1) + + // 胶片背景 + RoundedRectangle(cornerRadius: 10) + .fill(stripColor.opacity(0.8)) + .frame(height: 160) + .overlay( + // 胶片齿孔 + HStack(spacing: spacing) { + ForEach(0.. CGFloat { + let baseDistance: CGFloat = 1000 + let speedFactor: CGFloat = 1.0 + + return baseDistance * speedFactor * progressCurve() + } + + // 中间正胶卷偏移量计算(向左移动) + private func calculateMiddleOffset() -> CGFloat { + let baseDistance: CGFloat = -1100 + let speedFactor: CGFloat = 1.05 + + return baseDistance * speedFactor * progressCurve() + } + + // 下方倾斜胶卷偏移量计算(向右移动) + private func calculateBottomOffset() -> CGFloat { + let baseDistance: CGFloat = 1000 + let speedFactor: CGFloat = 0.95 + + return baseDistance * speedFactor * progressCurve() + } + + // 动画曲线:先慢后快,最后卡顿 + private func progressCurve() -> CGFloat { + if animationProgress < 0.6 { + // 初期加速阶段 + return easeInQuad(animationProgress / 0.6) * 0.7 + } else if animationProgress < 0.85 { + // 高速移动阶段 + return 0.7 + easeOutQuad((animationProgress - 0.6) / 0.25) * 0.25 + } else { + // 卡顿阶段 + let t = (animationProgress - 0.85) / 0.15 + return 0.95 + t * 0.05 + } + } + + // 缓入曲线 + private func easeInQuad(_ t: CGFloat) -> CGFloat { + return t * t + } + + // 缓出曲线 + private func easeOutQuad(_ t: CGFloat) -> CGFloat { + return t * (2 - t) + } + + // 启动动画序列 + private func startAnimation() { + // 第一阶段:逐渐加速 + withAnimation(.easeIn(duration: 3.5)) { + animationProgress = 0.6 + } + + // 第二阶段:高速移动 + DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) { + withAnimation(.linear(duration: 2.5)) { + animationProgress = 0.85 + } + + // 第三阶段:卡顿效果 + DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) { + withAnimation(.easeOut(duration: 1.8)) { + animationProgress = 1.0 + isCatching = true + } + + // 卡顿后重合消失,显示目标图片 + DispatchQueue.main.asyncAfter(deadline: .now() + 1.8) { + withAnimation(.easeInOut(duration: 0.7)) { + isDisappearing = true + } + + // 显示重复播放按钮 + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + withAnimation(.easeInOut(duration: 0.3)) { + showReplayButton = true + } + } + } + } + } + } +} + +// 电影胶卷视图组件 +struct FilmReelView1: View { + let images: [String] + + var body: some View { + HStack(spacing: 10) { + ForEach(images.indices, id: \.self) { index in + ZStack { + // 胶卷边框 + RoundedRectangle(cornerRadius: 4) + .stroke(Color.gray, lineWidth: 2) + .background(Color(red: 0.15, green: 0.15, blue: 0.15)) + + // 图片内容 + Rectangle() + .fill( + LinearGradient( + gradient: Gradient(colors: [.blue, .indigo]), + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .opacity(0.9) + .cornerRadius(2) + .padding(2) + + // 模拟图片文本 + Text("\(images[index])") + .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) + } + } + } + ) + } + } + } +} + +// 预览 +struct ReplayableFilmReelAnimation_Previews: PreviewProvider { + static var previews: some View { + ReplayableFilmReelAnimation() + } +} + \ No newline at end of file diff --git a/wake/View/Blind/Box3.swift b/wake/View/Blind/Box3.swift new file mode 100644 index 0000000..c482278 --- /dev/null +++ b/wake/View/Blind/Box3.swift @@ -0,0 +1,226 @@ +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() + } +} + \ No newline at end of file diff --git a/wake/View/Blind/Box4.swift b/wake/View/Blind/Box4.swift new file mode 100644 index 0000000..0ef9004 --- /dev/null +++ b/wake/View/Blind/Box4.swift @@ -0,0 +1,140 @@ +import SwiftUI + +// MARK: - 主视图:电影胶卷盲盒动效 +struct FilmStripBlindBoxView: View { + @State private var isAnimating = false + @State private var revealCenter = false + + // 三格盲盒内容(使用 SF Symbols 模拟不同“隐藏款”) + let boxContents = ["popcorn", "star", "music.note"] + + var body: some View { + GeometryReader { geometry in + let width = geometry.size.width + + ZStack { + // 左边盲盒胶卷帧 + BlindBoxFrame(symbol: boxContents[0]) + .offset(x: isAnimating ? -width / 4 : -width) + .opacity(isAnimating ? 1 : 0) + + // 中间盲盒胶卷帧(最终放大) + BlindBoxFrame(symbol: boxContents[1]) + .scaleEffect(revealCenter ? 1.6 : 1) + .offset(x: isAnimating ? 0 : width) + .opacity(isAnimating ? 1 : 0) + + // 右边盲盒胶卷帧 + BlindBoxFrame(symbol: boxContents[2]) + .offset(x: isAnimating ? width / 4 : width * 1.5) + .opacity(isAnimating ? 1 : 0) + } + .onAppear { + // 第一阶段:胶卷滑入 + withAnimation(.easeOut(duration: 1.0)) { + isAnimating = true + } + + // 第二阶段:中间帧“开盒”放大 + DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { + withAnimation( + .interpolatingSpring(stiffness: 80, damping: 12).delay(0.3) + ) { + revealCenter = true + } + } + } + } + .frame(height: 140) + .padding() + .background(Color.black.opacity(0.05)) + } +} + +// MARK: - 盲盒胶卷帧:带孔 + 橙色背景 + SF Symbol +struct BlindBoxFrame: View { + let symbol: String + + var body: some View { + ZStack { + // 胶片边框(橙色 + 打孔) + FilmBorder() + + // SF Symbol 作为“盲盒内容” + Image(systemName: symbol) + .resizable() + .scaledToFit() + .foregroundColor(.white.opacity(0.85)) + .frame(width: 60, height: 60) + } + .frame(width: 120, height: 120) + } +} + +// MARK: - 胶片边框:#FFB645 背景 + 打孔 +struct FilmBorder: View { + var body: some View { + Canvas { context, size in + let w = size.width + let h = size.height + + // 背景色:FFB645 + let bgColor = Color(hex: 0xFFB645) + context.fill(Path(CGRect(origin: .zero, size: size)), with: .color(bgColor)) + + // 打孔参数 + let holeRadius: CGFloat = 3.5 + let margin: CGFloat = 12 + let holeYOffset: CGFloat = h * 0.25 + + // 左侧打孔(3个) + for i in 0..<3 { + let y = CGFloat(i + 1) * (h / 4) + context.fill( + Path(ellipseIn: CGRect( + x: margin - holeRadius * 2, + y: y - holeRadius, + width: holeRadius * 2, + height: holeRadius * 2 + )), + with: .color(.black) + ) + } + + // 右侧打孔(3个) + for i in 0..<3 { + let y = CGFloat(i + 1) * (h / 4) + context.fill( + Path(ellipseIn: CGRect( + x: w - margin, + y: y - holeRadius, + width: holeRadius * 2, + height: holeRadius * 2 + )), + with: .color(.black) + ) + } + } + } +} + +// MARK: - Color 扩展:支持 HEX 颜色 +extension Color { + init(hex: UInt) { + self.init( + .sRGB, + red: Double((hex >> 16) & 0xff) / 255, + green: Double((hex >> 8) & 0xff) / 255, + blue: Double(hex & 0xff) / 255, + opacity: 1.0 + ) + } +} + +// MARK: - 预览 +struct FilmStripBlindBoxView_Previews: PreviewProvider { + static var previews: some View { + FilmStripBlindBoxView() + .preferredColorScheme(.dark) + } +} \ No newline at end of file diff --git a/wake/View/Blind/Box5.swift b/wake/View/Blind/Box5.swift new file mode 100644 index 0000000..93dd160 --- /dev/null +++ b/wake/View/Blind/Box5.swift @@ -0,0 +1,222 @@ +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() + } +} + \ No newline at end of file diff --git a/wake/View/Blind/Box6.swift b/wake/View/Blind/Box6.swift new file mode 100644 index 0000000..5ba1d07 --- /dev/null +++ b/wake/View/Blind/Box6.swift @@ -0,0 +1,250 @@ +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 + + // 胶卷数据 + private let reelImages: [[String]] = [ + (0..<300).map { "film1-\($0+1)" }, // 上方胶卷 + (0..<350).map { "film2-\($0+1)" }, // 中间胶卷 + (0..<300).map { "film3-\($0+1)" } // 下方胶卷 + ] + + // 胶卷参数 + private let frameWidth: CGFloat = 90 + private let frameHeight: CGFloat = 130 + private let frameSpacing: CGFloat = 12 + + // 动画阶段时间参数 + 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 var accelerationEnd: CGFloat { accelerationDuration / totalDuration } + private var constantSpeedEnd: CGFloat { (accelerationDuration + constantSpeedDuration) / totalDuration } + private var scaleStartEnd: CGFloat { + (accelerationDuration + constantSpeedDuration + scaleStartDuration) / totalDuration + } + + // 布局与运动参数(核心:对称倾斜角度) + 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 // 中间胶卷最大移动距离 + + 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() + } + } + + // MARK: - 动画逻辑 + + private func startAnimation() { + guard !isAnimating && !animationComplete else { return } + isAnimating = true + + withAnimation(Animation.easeInOut(duration: totalDuration)) { + animationProgress = 1.0 + } + + DispatchQueue.main.asyncAfter(deadline: .now() + totalDuration) { + isAnimating = false + animationComplete = true + } + } + + // 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 + } else { + return 1.0 // 6秒后停止移动 + } + } + + // 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 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 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: - 胶卷组件 + +struct FilmReelView: View { + let images: [String] + + var body: some View { + HStack(spacing: 12) { + ForEach(images.indices, id: \.self) { index in + FilmFrameView(imageName: images[index]) + } + } + } +} + +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) + } + } + } + ) + } + + 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) + } + } +} + +// 预览 +struct FilmAnimation_Previews: PreviewProvider { + static var previews: some View { + FilmAnimation() + } +} + diff --git a/wake/View/Welcome/SplashView.swift b/wake/View/Welcome/SplashView.swift index c81fbb6..463e6c4 100644 --- a/wake/View/Welcome/SplashView.swift +++ b/wake/View/Welcome/SplashView.swift @@ -19,57 +19,7 @@ struct SplashView: View { ) .edgesIgnoringSafeArea(.all) VStack(spacing: 50) { - Spacer() - // 欢迎文字动画 - Text("Welcome") - .font(.system(size: 40, weight: .bold, design: .rounded)) - .foregroundColor(.primary) - .scaleEffect(isAnimating ? 1.1 : 0.9) - .opacity(isAnimating ? 1 : 0.3) - .animation( - .easeInOut(duration: 1.5) - .repeatForever(autoreverses: true), - value: isAnimating - ) - - // 动画图标 - Image(systemName: "moon.stars.fill") - .font(.system(size: 120)) - .foregroundColor(.accentColor) - .rotationEffect(.degrees(isAnimating ? 360 : 0)) - .scaleEffect(isAnimating ? 1.2 : 0.8) - .animation( - .easeInOut(duration: 2) - .repeatForever(autoreverses: true), - value: isAnimating - ) - - Spacer() - - // 圆形按钮 - Button(action: { - withAnimation { - showLogin = true - } - }) { - Image(systemName: "arrow.right") - .font(.title) - .foregroundColor(.white) - .frame(width: 140, height: 140) - .background( - Circle() - .fill(Color.accentColor.opacity(0.7)) // 80% opacity - .shadow(radius: 10) - ) - } - .padding(.bottom, 40) - .background( - NavigationLink(destination: LoginView().environmentObject(authState), isActive: $showLogin) { - EmptyView() - } - .hidden() - ) - Spacer() + FilmAnimation() } .padding() } diff --git a/wake/WakeApp.swift b/wake/WakeApp.swift index f603882..baf704e 100644 --- a/wake/WakeApp.swift +++ b/wake/WakeApp.swift @@ -36,10 +36,10 @@ struct WakeApp: App { // 显示启动页 SplashView() .environmentObject(authState) - .onAppear { - // 启动页显示时检查token有效性 - checkTokenValidity() - } + // .onAppear { + // // 启动页显示时检查token有效性 + // checkTokenValidity() + // } } else { // 根据登录状态显示不同视图 if authState.isAuthenticated { @@ -55,14 +55,14 @@ struct WakeApp: App { } } } - .onAppear { - //3秒后自动隐藏启动页 - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - withAnimation { - showSplash = false - } - } - } + // .onAppear { + // //3秒后自动隐藏启动页 + // DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + // withAnimation { + // showSplash = false + // } + // } + // } } .modelContainer(container) } @@ -93,10 +93,10 @@ struct WakeApp: App { } // 3秒后自动隐藏启动页 - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - withAnimation { - showSplash = false - } - } + // DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + // withAnimation { + // showSplash = false + // } + // } } } \ No newline at end of file