wake-ios/wake/View/Blind/Box7.swift
2025-08-24 13:26:04 +08:00

479 lines
17 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
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 //
// - 使svgNamefullImageName
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")
}
}