140 lines
4.3 KiB
Swift
140 lines
4.3 KiB
Swift
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)
|
||
}
|
||
} |