wake-ios/wake/View/Blind/Box6.swift
2025-08-22 18:58:08 +08:00

251 lines
8.8 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()
}
}