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