wake-ios/wake/View/Test/TestView.swift
2025-08-22 20:16:16 +08:00

560 lines
25 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
// 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..<count {
let x = CGFloat.random(in: 0...size.width)
let y = CGFloat.random(in: 0...size.height)
let r = CGFloat.random(in: 0.3...0.8)
let alpha = Double.random(in: 0.15...0.55)
let color = Color.black.opacity(alpha)
context.fill(Path(ellipseIn: CGRect(x: x, y: y, width: r, height: r)), with: .color(color))
}
//
context.stroke(Path(rect), with: .color(.black.opacity(0.08)), lineWidth: 0.5)
}
.opacity(opacity)
.blendMode(.multiply)
}
}
// MARK: -
private struct EdgeFibers: View {
var body: some View {
GeometryReader { geo in
let w = geo.size.width
let h = geo.size.height
Canvas { context, _ in
// 线
let fibers = Int((w + h) * 1.6) //
for i in 0..<fibers {
let side = i % 4
let len: CGFloat = .random(in: 5...14)
let offset: CGFloat = .random(in: 0...((side % 2 == 0) ? w : h))
let inset: CGFloat = .random(in: 0...10)
var p = Path()
var start = CGPoint.zero
var end = CGPoint.zero
switch side {
case 0: // top
start = CGPoint(x: offset, y: inset)
end = CGPoint(x: offset + .random(in: -1...1), y: inset + len)
case 1: // right
start = CGPoint(x: w - inset, y: offset)
end = CGPoint(x: w - inset - len, y: offset + .random(in: -1...1))
case 2: // bottom
start = CGPoint(x: offset, y: h - inset)
end = CGPoint(x: offset + .random(in: -1...1), y: h - inset - len)
default: // left
start = CGPoint(x: inset, y: offset)
end = CGPoint(x: inset + len, y: offset + .random(in: -1...1))
}
p.move(to: start)
p.addLine(to: end)
let tone = Color(hex: "A68E72").opacity(Double.random(in: 0.08...0.18))
context.stroke(p, with: .color(tone), lineWidth: CGFloat.random(in: 0.4...1.2))
}
//
let insetBand: CGFloat = 2
let rr = RoundedRectangle(cornerRadius: 14, style: .continuous)
let outer = Path(rr.path(in: CGRect(x: 0, y: 0, width: w, height: h)).cgPath)
let inner = Path(rr.path(in: CGRect(x: insetBand, y: insetBand, width: w - insetBand*2, height: h - insetBand*2)).cgPath)
var ring = outer
ring.addPath(inner)
context.fill(ring, with: .color(Color.black.opacity(0.04)))
}
.blendMode(.multiply)
.opacity(1)
}
}
}
// MARK: -
private struct DeckleShape: Shape {
var cornerRadius: CGFloat
var amplitude: CGFloat //
var frequency: Int //
var seed: UInt64 = 0
func path(in rect: CGRect) -> 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..<count {
let side = i % 4
let offset = CGFloat.random(in: 0...((side % 2 == 0) ? w : h))
let jitter: CGFloat = .random(in: -2...2)
let dist: CGFloat = .random(in: -1.8...1.8) //
var center = CGPoint.zero
switch side {
case 0: center = CGPoint(x: offset, y: 0 + jitter + dist) // top
case 1: center = CGPoint(x: w + jitter - dist, y: offset) // right
case 2: center = CGPoint(x: offset, y: h + jitter - dist) // bottom
default: center = CGPoint(x: 0 + jitter + dist, y: offset) // left
}
let rw = CGFloat.random(in: 0.6...1.6)
let rh = CGFloat.random(in: 0.6...1.6)
let rect = CGRect(x: center.x - rw/2, y: center.y - rh/2, width: rw, height: rh)
let alpha = Double.random(in: 0.12...0.35)
//
context.fill(Path(ellipseIn: rect), with: .color(Color.black.opacity(alpha * 0.6)))
context.fill(Path(ellipseIn: rect.insetBy(dx: -0.4, dy: -0.4)), with: .color(Color.white.opacity(alpha * 0.35)))
}
}
// /
.blendMode(.overlay)
.overlay(
Canvas { context, _ in
// 沿
let steps = 220
for i in 0..<steps {
let t = CGFloat(i) / CGFloat(steps)
// roundedRect 线
var pt = CGPoint.zero
let per = t * 4
let edge = Int(per)
let u = per - CGFloat(edge)
switch edge {
case 0: pt = CGPoint(x: u * w, y: 0)
case 1: pt = CGPoint(x: w, y: u * h)
case 2: pt = CGPoint(x: (1 - u) * w, y: h)
default: pt = CGPoint(x: 0, y: (1 - u) * h)
}
let offset = CGPoint(x: pt.x + .random(in: -2...2), y: pt.y + .random(in: -2...2))
let s = CGFloat.random(in: 0.3...1.0)
context.fill(Path(ellipseIn: CGRect(x: offset.x, y: offset.y, width: s, height: s)), with: .color(Color.black.opacity(0.08)))
}
}
.blendMode(.multiply)
)
}
.allowsHitTesting(false)
.clipShape(DeckleShape(cornerRadius: cornerRadius, amplitude: amplitude, frequency: frequency, seed: seed))
}
}
// MARK: -
private struct InnerShadow: ViewModifier {
var cornerRadius: CGFloat
var shadow: Color
var radius: CGFloat
var x: CGFloat
var y: CGFloat
func body(content: Content) -> 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()
}
}