import SwiftUI import UIKit // MARK: - TestView /// 高质感试验页面:暖色景深背景 + 纸质相片卡片 + 滑块与胶囊按钮 struct TestView: View { @State private var slider: Double = 0.6 var body: some View { ZStack { // 背景:暖色渐变 + 散景光斑 BackgroundBokeh() .ignoresSafeArea() ScrollView(showsIndicators: false) { VStack(spacing: 20) { // 顶部标题区域(手写风格) VStack(spacing: 6) { Text("MemoryWake") .font(Typography.font(for: .headline, family: .quicksandBold, size: 30)) .kerning(1.2) .foregroundStyle(Color.black.opacity(0.85)) .shadow(color: .black.opacity(0.06), radius: 6, x: 0, y: 3) } .padding(.top, 26) // 纸质相片卡片 PaperPhotoCard() .padding(.horizontal, 24) .padding(.top, 6) // 年份滑块(带毛玻璃底) // VStack(spacing: 12) { // ZStack { // RoundedRectangle(cornerRadius: 16, style: .continuous) // .fill(.ultraThinMaterial) // .overlay { RoundedRectangle(cornerRadius: 16).stroke(Color.white.opacity(0.35), lineWidth: 0.5) } // .shadow(color: .black.opacity(0.1), radius: 12, x: 0, y: 6) // VStack(spacing: 8) { // HStack { // Text("2014") // .font(Typography.font(for: .caption)) // .foregroundStyle(Color.black.opacity(0.55)) // Spacer() // Text("2024") // .font(Typography.font(for: .caption)) // .foregroundStyle(Color.black.opacity(0.55)) // } // .padding(.horizontal, 14) // Slider(value: $slider, in: 0...1) // .tint(Theme.Colors.primary) // .padding(.horizontal, 14) // .padding(.bottom, 8) // } // .padding(.vertical, 8) // } // .frame(height: 76) // .padding(.horizontal, 24) // // 胶囊按钮 // Button { // // Action placeholder // } label: { // Text("Oc cinor rnand") // .font(Typography.font(for: .body, family: .quicksandBold)) // .foregroundStyle(Color.white) // .padding(.vertical, 14) // .frame(maxWidth: .infinity) // .background( // Capsule(style: .continuous) // .fill(LinearGradient(colors: [Theme.Colors.primaryDark, Theme.Colors.primary], startPoint: .leading, endPoint: .trailing)) // ) // .overlay { Capsule().stroke(Color.white.opacity(0.3), lineWidth: 0.6) } // .shadow(color: Theme.Shadows.medium, radius: 10, x: 0, y: 6) // } // .padding(.horizontal, 48) // .padding(.bottom, 24) // } } .padding(.bottom, 40) } } } } // MARK: - 背景散景 private struct BackgroundBokeh: View { var body: some View { ZStack { Theme.Gradients.backgroundGradient .ignoresSafeArea() .overlay(Color.white.opacity(0.35).blendMode(.softLight)) // 大散景圆(Canvas 生成,带模糊 & 叠加) Canvas { context, size in func circle(_ center: CGPoint, _ radius: CGFloat, _ color: Color) { let rect = CGRect(x: center.x - radius, y: center.y - radius, width: radius * 2, height: radius * 2) context.fill(Path(ellipseIn: rect), with: .color(color.opacity(0.45))) } circle(CGPoint(x: size.width * 0.2, y: size.height * 0.2), 90, .white) circle(CGPoint(x: size.width * 0.85, y: size.height * 0.18), 70, .white) circle(CGPoint(x: size.width * 0.75, y: size.height * 0.65), 110, .white) circle(CGPoint(x: size.width * 0.15, y: size.height * 0.75), 80, .white) } .blur(radius: 22) .blendMode(.screen) .opacity(0.8) } } } // MARK: - 纸质相片卡片 private struct PaperPhotoCard: View { var body: some View { VStack(alignment: .leading, spacing: 12) { ZStack { // 纸质底(多层次质感 + 不规则撕边遮罩) let radius: CGFloat = 14 let deckle = DeckleShape(cornerRadius: radius, amplitude: 2.0, frequency: 48, seed: 1337) deckle .fill( LinearGradient(colors: [Color.white, Color(hex: "F5EEE4")], startPoint: .top, endPoint: .bottom) ) // 轻微纸张噪点 .overlay { NoiseOverlay(opacity: 0.09) .clipShape(deckle) } // 边缘高光(上左) .overlay { deckle .stroke(LinearGradient(colors: [Color.white.opacity(0.65), .clear], startPoint: .topLeading, endPoint: .bottomTrailing), lineWidth: 1) } // 边缘阴影(下右) .overlay { deckle .stroke(LinearGradient(colors: [.clear, Color.black.opacity(0.12)], startPoint: .topLeading, endPoint: .bottomTrailing), lineWidth: 1) } // 纤维毛边/撕边的错乱感(叠加一些短纤维线条) .overlay { EdgeFibers().clipShape(deckle) } // 更明显的毛边凹凸(微小颗粒沿轮廓散布) .overlay { DeckleOverlay(cornerRadius: radius, amount: 1.6, amplitude: 2.0, frequency: 48, seed: 1337) } // 内阴影,形成纸张厚度 .modifier(InnerShadow(cornerRadius: radius, shadow: .black.opacity(0.15), radius: 10, x: 0, y: 4)) // 外投影与微浮雕(参考图:光源偏右上) .shadow(color: .black.opacity(0.16), radius: 12, x: 0, y: 10) // 基础下方投影(范围更窄) // 加重左下与右下两侧的阴影(左下略重,右下略轻) .shadow(color: .black.opacity(0.22), radius: 8, x: -4, y: 7) // 左下更重(范围更窄) .shadow(color: .black.opacity(0.18), radius: 8, x: 3, y: 8) // 右下略轻(范围更窄) // 左侧轻微遮蔽(环境遮挡) .shadow(color: .black.opacity(0.12), radius: 2, x: -2, y: 2) .shadow(color: .white.opacity(0.7), radius: 1, x: -1, y: -1) VStack(alignment: .leading, spacing: 12) { // 相片 ImageAtPath(path: "/Users/fairclip/Documents/Projects/wake-ios/wake.xcodeproj/xcuserdata/fairclip.xcuserdatad/screenshot-20250821-154302.png") .clipShape(DeckleShape(cornerRadius: 8, amplitude: 0.8, frequency: 32, seed: 1338)) .overlay { DeckleShape(cornerRadius: 8, amplitude: 0.8, frequency: 32, seed: 1338).stroke(Color.black.opacity(0.12), lineWidth: 0.6) } .shadow(color: .black.opacity(0.12), radius: 8, x: 0, y: 4) .padding(.top, 14) .clipped() HStack(alignment: .bottom) { VStack(alignment: .leading, spacing: 4) { Text("You uploaded your portrait, and chose to remember the moments you almost let slip away.") .font(Typography.font(for: .title, family: .lavishlyYours)) .foregroundStyle(Color.black.opacity(0.45)) } } .padding(.horizontal, 14) .padding(.bottom, 26) } .padding(.horizontal, 12) // 底部轻微缺口(撕口提示) // VStack { Spacer(minLength: 0) // Capsule(style: .continuous) // .fill(Color.black.opacity(0.08)) // .frame(width: 36, height: 8) // .blur(radius: 0.2) // .offset(y: 10) // } } } } } // MARK: - 相片内容(木箱 + 景深效果,用渐变和阴影模拟) private struct ImageCard: View { var body: some View { ZStack { // 地面与远景 LinearGradient(colors: [Color(hex: "E6E0D8"), Color(hex: "C9BBAA")], startPoint: .top, endPoint: .bottom) .overlay( // 右上角光束 RadialGradient(colors: [Color.white.opacity(0.75), .clear], center: .topTrailing, startRadius: 4, endRadius: 220) .blendMode(.screen) ) // 木箱(简化几何) VStack(spacing: 0) { RoundedRectangle(cornerRadius: 3).fill(Color(hex: "7B5134")) .frame(height: 54) .overlay { LinearGradient(colors: [Color(hex: "9C6A46"), Color(hex: "5E3D28")], startPoint: .topLeading, endPoint: .bottomTrailing) .clipShape(RoundedRectangle(cornerRadius: 3)) } .shadow(color: .black.opacity(0.25), radius: 10, x: 0, y: 8) RoundedRectangle(cornerRadius: 3).fill(Color(hex: "6A462F")) .frame(height: 20) } .padding(.horizontal, 22) .padding(.vertical, 18) // 小单车(符号占位) Image(systemName: "bicycle") .resizable() .scaledToFit() .frame(width: 36, height: 36) .foregroundStyle(Color.white) .shadow(color: .black.opacity(0.35), radius: 6, x: 0, y: 4) } .frame(height: 400) .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) } } // MARK: - 噪点叠加(轻微纸张颗粒) private struct NoiseOverlay: View { let opacity: CGFloat var body: some View { Canvas { context, size in let rect = CGRect(origin: .zero, size: size) // 绘制随机小点 let count = Int(size.width * size.height / 800) for _ in 0.. Path { let r = min(cornerRadius, min(rect.width, rect.height) / 2) let steps = max(8, frequency) var points: [CGPoint] = [] // 简单线性同余随机(可复现) var state = seed == 0 ? 0x9E3779B97F4A7C15 : seed func rand() -> CGFloat { state &+= 0xBF58476D1CE4E5B9 var z = state z = (z ^ (z >> 30)) &* 0xBF58476D1CE4E5B9 z = (z ^ (z >> 27)) &* 0x94D049BB133111EB let v = Double((z ^ (z >> 31)) & 0x1FFFFFFFFFFFFF) / Double(0x1FFFFFFFFFFFFF) return CGFloat(v) } func jitter(_ normal: CGPoint) -> CGPoint { let a = (rand() * 2 - 1) * amplitude return CGPoint(x: normal.x * a, y: normal.y * a) } // 边与角的基础点生成 let left = rect.minX let right = rect.maxX let top = rect.minY let bottom = rect.maxY // 四条边: top -> right -> bottom -> left // 直边部分(去掉角落 r) // Top straight if rect.width > 2*r { for i in 0...steps { let t = CGFloat(i) / CGFloat(steps) let x = left + r + t * (rect.width - 2*r) let base = CGPoint(x: x, y: top) let n = CGPoint(x: 0, y: -1) points.append(base + jitter(n)) } } // Top-right corner arc let arcSteps = max(6, steps/3) for i in 0...arcSteps { let ang = CGFloat.pi * 1.5 + CGFloat(i) / CGFloat(arcSteps) * (.pi/2) let center = CGPoint(x: right - r, y: top + r) let base = CGPoint(x: center.x + cos(ang) * r, y: center.y + sin(ang) * r) let normal = CGPoint(x: cos(ang), y: sin(ang)) points.append(base + jitter(normal)) } // Right straight if rect.height > 2*r { for i in 0...steps { let t = CGFloat(i) / CGFloat(steps) let y = top + r + t * (rect.height - 2*r) let base = CGPoint(x: right, y: y) let n = CGPoint(x: 1, y: 0) points.append(base + jitter(n)) } } // Bottom-right arc for i in 0...arcSteps { let ang = 0 + CGFloat(i) / CGFloat(arcSteps) * (.pi/2) let center = CGPoint(x: right - r, y: bottom - r) let base = CGPoint(x: center.x + cos(ang) * r, y: center.y + sin(ang) * r) let normal = CGPoint(x: cos(ang), y: sin(ang)) points.append(base + jitter(normal)) } // Bottom straight if rect.width > 2*r { for i in 0...steps { let t = CGFloat(i) / CGFloat(steps) let x = right - r - t * (rect.width - 2*r) let base = CGPoint(x: x, y: bottom) let n = CGPoint(x: 0, y: 1) points.append(base + jitter(n)) } } // Bottom-left arc for i in 0...arcSteps { let ang = .pi/2 + CGFloat(i) / CGFloat(arcSteps) * (.pi/2) let center = CGPoint(x: left + r, y: bottom - r) let base = CGPoint(x: center.x + cos(ang) * r, y: center.y + sin(ang) * r) let normal = CGPoint(x: cos(ang), y: sin(ang)) points.append(base + jitter(normal)) } // Left straight if rect.height > 2*r { for i in 0...steps { let t = CGFloat(i) / CGFloat(steps) let y = bottom - r - t * (rect.height - 2*r) let base = CGPoint(x: left, y: y) let n = CGPoint(x: -1, y: 0) points.append(base + jitter(n)) } } // Top-left arc for i in 0...arcSteps { let ang = .pi + CGFloat(i) / CGFloat(arcSteps) * (.pi/2) let center = CGPoint(x: left + r, y: top + r) let base = CGPoint(x: center.x + cos(ang) * r, y: center.y + sin(ang) * r) let normal = CGPoint(x: cos(ang), y: sin(ang)) points.append(base + jitter(normal)) } // 连接为路径 var p = Path() guard let first = points.first else { return p } p.move(to: first) for pt in points.dropFirst() { p.addLine(to: pt) } p.closeSubpath() return p } } // CGPoint 运算便捷 private extension CGPoint { static func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint { CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y) } } // MARK: - 毛边凹凸/颗粒覆盖 private struct DeckleOverlay: View { var cornerRadius: CGFloat var amount: CGFloat = 1.0 // 0.5~2.0 建议范围 var amplitude: CGFloat = 2.0 var frequency: Int = 48 var seed: UInt64 = 0 var body: some View { GeometryReader { geo in let w = geo.size.width let h = geo.size.height Canvas { context, _ in // 沿周边撒布很多小圆点和短椭圆,部分向外偏移、部分向内侵蚀 let perimeter = 2 * (w + h) let count = Int(CGFloat(perimeter) / 4 * max(0.4, amount)) for i in 0.. some View { content .overlay( RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) .stroke(Color.clear, lineWidth: 0) .shadow(color: shadow, radius: radius, x: x, y: y) .clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)) .blendMode(.multiply) ) } } #Preview("TestView - Light") { TestView() } #Preview("TestView - Dark") { TestView() .preferredColorScheme(.dark) } // MARK: - 从绝对路径加载图片(仅用于开发/预览;真机请放入 Assets) private struct ImageAtPath: View { let path: String var body: some View { Group { if let ui = UIImage(contentsOfFile: path) { Image(uiImage: ui) .resizable() .scaledToFit() } else { ZStack { Color.black.opacity(0.04) VStack(spacing: 6) { Image(systemName: "photo") .font(.system(size: 28)) .foregroundStyle(Color.black.opacity(0.4)) Text("未找到图片") .font(.caption) .foregroundStyle(Color.black.opacity(0.5)) Text(path) .font(.caption2) .foregroundStyle(Color.black.opacity(0.4)) .multilineTextAlignment(.center) .lineLimit(2) } .padding() } .clipped() } } .frame(maxWidth: .infinity) .clipped() } }