diff --git a/.gitignore b/.gitignore index e69de29..fd1b780 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1 @@ +wake.xcodeproj/xcuserdata/fairclip.xcuserdatad/screenshot-20250821-154302.png diff --git a/wake.xcodeproj/project.xcworkspace/xcuserdata/fairclip.xcuserdatad/UserInterfaceState.xcuserstate b/wake.xcodeproj/project.xcworkspace/xcuserdata/fairclip.xcuserdatad/UserInterfaceState.xcuserstate index 754cc0d..af67373 100644 Binary files a/wake.xcodeproj/project.xcworkspace/xcuserdata/fairclip.xcuserdatad/UserInterfaceState.xcuserstate and b/wake.xcodeproj/project.xcworkspace/xcuserdata/fairclip.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/wake/ContentView.swift b/wake/ContentView.swift index 2d9ecaf..f85ebda 100644 --- a/wake/ContentView.swift +++ b/wake/ContentView.swift @@ -80,6 +80,17 @@ struct ContentView: View { .cornerRadius(8) } + // 测试质感页面入口 + NavigationLink(destination: TestView()) { + Text("TestView") + .font(.subheadline) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(Color.brown) + .foregroundColor(.white) + .cornerRadius(8) + } + // 订阅测试按钮 NavigationLink(destination: SubscribeView()) { Text("Subscribe") diff --git a/wake/Info.plist b/wake/Info.plist index 23b998e..8e8cc04 100644 --- a/wake/Info.plist +++ b/wake/Info.plist @@ -29,6 +29,7 @@ Quicksand-SemiBold.ttf Quicksand-Medium.ttf Quicksand-Light.ttf + LavishlyYours-Regular.ttf diff --git a/wake/Resources/Fonts/LavishlyYours-Regular.ttf b/wake/Resources/Fonts/LavishlyYours-Regular.ttf new file mode 100644 index 0000000..8e91e7d Binary files /dev/null and b/wake/Resources/Fonts/LavishlyYours-Regular.ttf differ diff --git a/wake/Typography.swift b/wake/Typography.swift index 44cacff..e655d70 100644 --- a/wake/Typography.swift +++ b/wake/Typography.swift @@ -6,6 +6,7 @@ enum FontFamily: String, CaseIterable { case sankeiCute = "SankeiCutePopanime" // 可爱风格字体 case quicksandRegular = "Quicksand-Regular" // 主题字体(常规) case quicksandBold = "Quicksand-Bold" + case lavishlyYours = "LavishlyYours-Regular" // 后续添加新字体库时在这里添加新 case // 例如: case anotherFont = "AnotherFontName" diff --git a/wake/View/Test/TestView.swift b/wake/View/Test/TestView.swift new file mode 100644 index 0000000..42c26a3 --- /dev/null +++ b/wake/View/Test/TestView.swift @@ -0,0 +1,559 @@ +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: .sankeiCute, 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() + } +}