walkerbuildapps

japan onboarding sample

Apr 11th, 2025
24
0
Never
1
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 20.93 KB | None | 0 0
  1. //made for r/keisen subreddit, mobile forward design spot ;)
  2. //join today,ill be posting r/keisen designs/sourcecode/tutorials in the future and a bunch of other cool stuff
  3. //have fun with this, use it for your own projects, destroy it, go nuts
  4.  
  5. import SwiftUI
  6. import UIKit
  7.  
  8. struct GeometricLine: Identifiable {
  9. var id: UUID = UUID()
  10. var startPoint: CGPoint
  11. var points: [CGPoint]
  12. var width: CGFloat
  13. var color: Color
  14. var opacity: Double
  15. }
  16.  
  17. struct GridCell: Identifiable {
  18. var id: UUID = UUID()
  19. var position: CGPoint
  20. var size: CGSize
  21. var color: Color
  22. var opacity: Double
  23. var rotation: Double
  24. }
  25.  
  26. struct BrutalistAssistantOption: Identifiable {
  27. var id: String
  28. var name: String
  29. var designation: String
  30. var description: String
  31. var primaryColor: Color
  32. var secondaryColor: Color
  33. var symbolName: String
  34. var signalData: [Double]
  35.  
  36. var tagline: String {
  37. description.split(separator: ".").first.map(String.init) ?? ""
  38. }
  39. }
  40.  
  41. struct RectangularShape: Shape {
  42. var cornerRadius: CGFloat = 0
  43.  
  44. func path(in rect: CGRect) -> Path {
  45. Path(roundedRect: rect, cornerSize: CGSize(width: cornerRadius, height: cornerRadius))
  46. }
  47. }
  48.  
  49. struct ConcreteTextureEffect: View {
  50. var intensity: Double
  51.  
  52. var body: some View {
  53. Canvas { context, size in
  54. for _ in 0..<500 {
  55. let x = CGFloat.random(in: 0...size.width)
  56. let y = CGFloat.random(in: 0...size.height)
  57. let radius = CGFloat.random(in: 0.5...1.5)
  58. let opacity = Double.random(in: 0.05...0.2) * intensity
  59. let color = Color.black.opacity(opacity)
  60. context.fill(Path(ellipseIn: CGRect(x: x, y: y, width: radius, height: radius)), with: .color(color))
  61. }
  62. }
  63. .allowsHitTesting(false)
  64. }
  65. }
  66.  
  67. struct MinimalSignalIndicator: View {
  68. var values: [Double]
  69. var color: Color
  70. @State private var animationPhase: Double = 0
  71.  
  72. var body: some View {
  73. Canvas { context, size in
  74. guard values.count > 1 else { return }
  75. var points: [CGPoint] = []
  76. let width = size.width
  77. let height = size.height
  78. let segments = values.count
  79.  
  80. for i in 0..<segments {
  81. let x = width * CGFloat(i) / CGFloat(segments - 1)
  82. let animatedValue = values[i] * (0.7 + 0.3 * sin(animationPhase * .pi * 2 + Double(i) * 0.5))
  83. let y = height * (1.0 - CGFloat(animatedValue))
  84. points.append(CGPoint(x: x, y: y))
  85. }
  86.  
  87. var path = Path()
  88. path.move(to: points[0])
  89. points.dropFirst().forEach { path.addLine(to: $0) }
  90.  
  91. context.stroke(path, with: .color(color), lineWidth: 1.0)
  92. }
  93. .onAppear {
  94. // make the line wiggle forever
  95. withAnimation(.linear(duration: 8).repeatForever(autoreverses: false)) {
  96. animationPhase = 1.0
  97. }
  98. }
  99. .allowsHitTesting(false)
  100. }
  101. }
  102.  
  103. struct JapaneseBrutalistView: View {
  104. @State private var backgroundProgress: Double = 0
  105. @State private var contentOpacity: Double = 0
  106. @State private var assistantCardsOpacity: Double = 0
  107. @State private var continueButtonOpacity: Double = 0
  108. @State private var continueButtonOffset: CGFloat = 25
  109. @State private var continueButtonScale: CGFloat = 1.0
  110. @State private var selectedOption: String? = nil
  111. @State private var descriptionOpacity: Double = 0
  112. @State private var geometricLines: [GeometricLine] = []
  113. @State private var gridCells: [GridCell] = []
  114. @State private var statusText = "システム読み込み中..."
  115. @State private var displayedStatusText = ""
  116. @State private var textPosition = 0
  117.  
  118. private let brutalistOptions = [
  119. BrutalistAssistantOption(id: "kado", name: "角", designation: "K-01", description: "直角と幾何学的精度。実用性と構造的完全性を重視する。", primaryColor: Color(white:0.01), secondaryColor: Color(white: 0.3), symbolName: "square.grid.3x3", signalData: [0.2, 0.7, 0.3, 0.9, 0.4, 0.6, 0.2]),
  120. BrutalistAssistantOption(id: "kabe", name: "壁", designation: "W-02", description: "堅牢な保護と耐久性。構造的強度と耐久性の基盤。", primaryColor: Color(white: 0.2), secondaryColor: Color(white: 0.4), symbolName: "square.split.2x2", signalData: [0.6, 0.3, 0.8, 0.2, 0.7, 0.4, 0.9]),
  121. BrutalistAssistantOption(id: "ishi", name: "石", designation: "S-03", description: "基本的な強さと安定性。純粋形態に込められた静寂の力。", primaryColor: Color(white: 0.15), secondaryColor: Color(white: 0.35), symbolName: "cube", signalData: [0.4, 0.8, 0.2, 0.5, 0.9, 0.3, 0.6])
  122. ]
  123.  
  124. private let brutalistColors = (
  125. background: Color(white: 0.95), concrete: Color(white: 0.85), darkConcrete: Color(white: 0.75),
  126. text: Color(white: 0.1), accent: Color(red: 0.7, green: 0.0, blue: 0.0),
  127. subtleText: Color(white: 0.3), highlight: Color(white: 1.0)
  128. )
  129.  
  130. var body: some View {
  131. GeometryReader { geometry in
  132. ZStack {
  133. // Background Layers
  134. ZStack {
  135. Rectangle().fill(brutalistColors.background).opacity(backgroundProgress)
  136. ConcreteTextureEffect(intensity: 0.7).opacity(backgroundProgress * 0.3)
  137. ForEach(geometricLines) { line in
  138. Path { path in
  139. path.move(to: line.startPoint)
  140. line.points.forEach { path.addLine(to: $0) }
  141. }
  142. .stroke(line.color.opacity(0.3 * line.opacity * backgroundProgress), style: StrokeStyle(lineWidth: line.width, lineCap: .square, lineJoin: .miter))
  143. }
  144. ForEach(gridCells) { cell in
  145. RectangularShape()
  146. .stroke(cell.color.opacity(cell.opacity * 0.2 * backgroundProgress), lineWidth: 1)
  147. .frame(width: cell.size.width, height: cell.size.height)
  148. .position(cell.position)
  149. .rotationEffect(.degrees(cell.rotation))
  150. }
  151. RadialGradient(gradient: Gradient(colors: [.clear, .black.opacity(0.1)]), center: .center, startRadius: geometry.size.width * 0.5, endRadius: geometry.size.width)
  152. .opacity(backgroundProgress * 0.7)
  153. .allowsHitTesting(false)
  154. }
  155. .ignoresSafeArea()
  156.  
  157. // Status Text (Top Right)
  158. VStack {
  159. HStack {
  160. Spacer()
  161. Text(displayedStatusText)
  162. .font(.system(.caption, design: .monospaced))
  163. .foregroundColor(brutalistColors.subtleText)
  164. .frame(height: 20)
  165. .padding(.horizontal, 20).padding(.top, 10)
  166. }
  167. Spacer()
  168. }
  169. .opacity(contentOpacity * 0.7)
  170. .allowsHitTesting(false)
  171.  
  172. // Main Content
  173. VStack(spacing: 0) {
  174. // Header
  175. HStack {
  176. Button(action: {
  177. UIImpactFeedbackGenerator(style: .rigid).impactOccurred()
  178. print("Back button pressed")
  179. }) {
  180. ZStack {
  181. RectangularShape().stroke(brutalistColors.text, lineWidth: 1).frame(width: 38, height: 38).background(brutalistColors.concrete)
  182. Image(systemName: "chevron.left").font(.system(size: 14, weight: .bold)).foregroundColor(brutalistColors.text)
  183. }
  184. }
  185. Spacer()
  186. Text("システム状態:アクティブ").font(.system(.caption, design: .monospaced)).tracking(2).foregroundColor(brutalistColors.subtleText).opacity(0.7).padding(.trailing, 10)
  187. }
  188. .padding(.horizontal, 20).padding(.top, 20).opacity(contentOpacity)
  189.  
  190. // Title Area
  191. VStack(spacing: 8) {
  192. Rectangle().fill(brutalistColors.text).frame(height: 4).padding(.horizontal, 40).padding(.bottom, 2)
  193. Text("選択").font(.system(.title2, design: .monospaced)).fontWeight(.bold).tracking(2).foregroundColor(brutalistColors.text)
  194. Text("コンクリート構造体").font(.system(.body, design: .monospaced)).tracking(1).foregroundColor(brutalistColors.text).opacity(0.7).offset(y: -4)
  195. Text("優先度:最高").font(.system(.caption, design: .monospaced)).tracking(2).padding(.horizontal, 12).padding(.vertical, 4)
  196. .background(RectangularShape().stroke(brutalistColors.text, lineWidth: 1).background(brutalistColors.concrete))
  197. .foregroundColor(brutalistColors.text)
  198. }
  199. .padding(.vertical, 25).opacity(contentOpacity)
  200.  
  201. // Option Cards
  202. VStack(spacing: 16) {
  203. ForEach(brutalistOptions) { option in
  204. BrutalistOptionCard(
  205. option: option, isSelected: selectedOption == option.id,
  206. onSelect: {
  207. UIImpactFeedbackGenerator(style: .rigid).impactOccurred()
  208. // update which card is picked and make it pop
  209. withAnimation(.spring(response: 0.3, dampingFraction: 0.9)) {
  210. selectedOption = option.id
  211. if descriptionOpacity == 0 { descriptionOpacity = 1 }
  212. }
  213. }
  214. )
  215. .scaleEffect(selectedOption == option.id ? 1.02 : 1.0)
  216. .animation(.spring(response: 0.3, dampingFraction: 0.9), value: selectedOption)
  217. }
  218. }
  219. .padding(.horizontal, 20).opacity(assistantCardsOpacity)
  220.  
  221. // Description Panel
  222. if let selectedId = selectedOption, let currentOption = brutalistOptions.first(where: { $0.id == selectedId }) {
  223. HStack(spacing: 15) {
  224. MinimalSignalIndicator(values: currentOption.signalData, color: currentOption.primaryColor)
  225. .frame(width: 50, height: 50).background(brutalistColors.concrete).border(currentOption.primaryColor, width: 1)
  226. Text(currentOption.description).font(.system(.caption, design: .monospaced)).tracking(0.5).foregroundColor(brutalistColors.text.opacity(0.9)).lineSpacing(4)
  227. }
  228. .padding(.horizontal, 20).padding(.vertical, 15)
  229. .background(RectangularShape().stroke(currentOption.primaryColor, lineWidth: 1).background(brutalistColors.concrete.opacity(0.5)))
  230. .padding(.horizontal, 20).padding(.top, 20)
  231. .opacity(descriptionOpacity)
  232. .transition(.opacity.combined(with: .move(edge: .top)))
  233. }
  234.  
  235. Spacer()
  236.  
  237. // Continue Button
  238. Button(action: {
  239. guard selectedOption != nil else { return }
  240. UIImpactFeedbackGenerator(style: .rigid).impactOccurred()
  241. withAnimation(.easeInOut(duration: 0.15)) { continueButtonScale = 0.98 }
  242. DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
  243. withAnimation(.easeOut(duration: 0.15)) { continueButtonScale = 1.0 }
  244. DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
  245. // just print for preview, normally would navigate
  246. print("Continue pressed with: \(selectedOption ?? "None")")
  247. }
  248. }
  249. }) {
  250. ZStack {
  251. Rectangle().fill(getSelectedOption()?.primaryColor ?? brutalistColors.concrete).frame(height: 52)
  252. .overlay(RectangularShape().stroke(brutalistColors.text, lineWidth: 1.5))
  253. HStack(spacing: 10) {
  254. Text("開始").font(.system(.body, design: .monospaced)).fontWeight(.bold).tracking(2).foregroundColor(brutalistColors.background)
  255. Image(systemName: "arrow.right").font(.system(size: 14, weight: .bold)).foregroundColor(brutalistColors.background)
  256. }.frame(maxWidth: .infinity)
  257. }.padding(.horizontal, 20)
  258. }
  259. .opacity(selectedOption == nil ? 0.5 : continueButtonOpacity)
  260. .scaleEffect(continueButtonScale).offset(y: continueButtonOffset).disabled(selectedOption == nil)
  261. .padding(.bottom, 50)
  262.  
  263. // Progress Indicator
  264. HStack(spacing: 20) {
  265. ForEach(0..<3) { index in
  266. Rectangle().fill(index == 1 ? brutalistColors.text : brutalistColors.darkConcrete).frame(width: 20, height: 3)
  267. }
  268. }
  269. .padding(.bottom, 15).opacity(contentOpacity * 0.8)
  270. }
  271. .padding(.top, UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0)
  272. }
  273. .onAppear {
  274. // slap some random lines and squares on the background
  275. generateGeometricLines(size: geometry.size)
  276. generateGridCells(size: geometry.size)
  277. startAnimations()
  278. }
  279. }
  280. .preferredColorScheme(.light)
  281. }
  282.  
  283. // MARK: - Animations & Helpers
  284. private func startAnimations() {
  285. let rigidHaptic = UIImpactFeedbackGenerator(style: .rigid)
  286. withAnimation(.easeIn(duration: 0.8)) { backgroundProgress = 1.0; rigidHaptic.impactOccurred(intensity: 0.5) }
  287. DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
  288. withAnimation(.easeOut(duration: 0.8)) { contentOpacity = 1.0 }
  289. DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
  290. withAnimation(.easeOut(duration: 0.8)) { assistantCardsOpacity = 1.0; rigidHaptic.impactOccurred(intensity: 0.4) }
  291. DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
  292. withAnimation(.easeOut(duration: 0.8)) { continueButtonOpacity = 1.0; continueButtonOffset = 0 }
  293. }
  294. }
  295. }
  296. startStatusTextAnimation()
  297. }
  298.  
  299. private func startStatusTextAnimation() {
  300. // type out the status text like an old terminal
  301. Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
  302. if textPosition < statusText.count {
  303. let index = statusText.index(statusText.startIndex, offsetBy: textPosition)
  304. displayedStatusText.append(statusText[index])
  305. textPosition += 1
  306. } else {
  307. timer.invalidate()
  308. DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
  309. displayedStatusText = ""
  310. textPosition = 0
  311. let texts = ["構造体読み込み中...", "システム準備完了", "選択を待機中...", "構造解析実行中...", "安定性確認済み"]
  312. statusText = texts.randomElement() ?? "状態確認中..."
  313. startStatusTextAnimation()
  314. }
  315. }
  316. }
  317. }
  318.  
  319. private func getSelectedOption() -> BrutalistAssistantOption? {
  320. brutalistOptions.first { $0.id == selectedOption }
  321. }
  322.  
  323. private func generateGeometricLines(size: CGSize) {
  324. geometricLines = (0..<15).map { _ in
  325. var points: [CGPoint] = []
  326. var currentPoint = CGPoint(x: .random(in: 0...size.width), y: .random(in: 0...size.height))
  327. points.append(currentPoint)
  328. for _ in 0..<Int.random(in: 2...4) {
  329. let isHorizontal = Bool.random()
  330. let distance = CGFloat.random(in: 40...120)
  331. currentPoint = CGPoint(
  332. x: currentPoint.x + (isHorizontal ? (Bool.random() ? distance : -distance) : 0),
  333. y: currentPoint.y + (!isHorizontal ? (Bool.random() ? distance : -distance) : 0)
  334. )
  335. points.append(currentPoint)
  336. }
  337. return GeometricLine(startPoint: points.first!, points: Array(points.dropFirst()), width: 1.0, color: Color(white: .random(in: 0.2...0.5)), opacity: .random(in: 0.2...0.4))
  338. }
  339. }
  340.  
  341. private func generateGridCells(size: CGSize) {
  342. gridCells = []
  343. let baseSize: CGFloat = 60, spacing: CGFloat = 80
  344. for row in 0...Int(size.height / spacing) + 1 {
  345. for col in 0...Int(size.width / spacing) + 1 {
  346. guard Double.random(in: 0...1) >= 0.4 else { continue }
  347. var width = baseSize, height = baseSize
  348. if Double.random(in: 0...1) < 0.15 { width *= .random(in: 1.2...1.8); height *= .random(in: 1.2...1.8) }
  349. let x = CGFloat(col) * spacing + .random(in: -5...5)
  350. let y = CGFloat(row) * spacing + .random(in: -5...5)
  351. let rotation = Double.random(in: 0...1) < 0.05 ? Double.random(in: -3...3) : 0.0
  352. gridCells.append(GridCell(position: CGPoint(x: x, y: y), size: CGSize(width: width, height: height), color: Color(white: .random(in: 0.2...0.4)), opacity: .random(in: 0.2...0.4), rotation: rotation))
  353. }
  354. }
  355. }
  356. }
  357.  
  358. struct BrutalistOptionCard: View {
  359. var option: BrutalistAssistantOption
  360. var isSelected: Bool
  361. var onSelect: () -> Void
  362.  
  363. private let cardColors = (concrete: Color(white: 0.85), text: Color(white: 0.1), background: Color(white: 0.95))
  364.  
  365. var body: some View {
  366. Button(action: onSelect) {
  367. ZStack {
  368. Rectangle().fill(isSelected ? option.primaryColor : cardColors.concrete)
  369. .overlay(Rectangle().stroke(cardColors.text, lineWidth: isSelected ? 1.5 : 1))
  370. HStack(spacing: 15) {
  371. VStack(spacing: 4) {
  372. ZStack {
  373. Rectangle().stroke(cardColors.text, lineWidth: 1).frame(width: 50, height: 50).background(isSelected ? option.primaryColor: cardColors.concrete)
  374. Image(systemName: option.symbolName).font(.system(size: 20, weight: .regular)).foregroundColor(isSelected ? cardColors.background : cardColors.text)
  375. }
  376. Text(option.designation).font(.system(.caption2, design: .monospaced)).foregroundColor(isSelected ? cardColors.background : cardColors.text).padding(.top, 4)
  377. }.frame(width: 60)
  378.  
  379. Rectangle().fill(isSelected ? cardColors.background : cardColors.text).frame(width: 1).padding(.vertical, 15)
  380.  
  381. VStack(alignment: .leading, spacing: 8) {
  382. Text(option.name).font(.system(.body, design: .monospaced)).fontWeight(.bold).tracking(1).foregroundColor(isSelected ? cardColors.background : cardColors.text)
  383. HStack(spacing: 10) {
  384. MinimalSignalIndicator(values: option.signalData, color: isSelected ? cardColors.background : cardColors.text).frame(width: 60, height: 20)
  385. Text(option.tagline).font(.system(.caption2, design: .monospaced)).foregroundColor(isSelected ? cardColors.background.opacity(0.8) : cardColors.text.opacity(0.8)).lineLimit(2)
  386. }
  387. }
  388. Spacer()
  389. ZStack {
  390. Rectangle().stroke(isSelected ? cardColors.background : cardColors.text, lineWidth: 1).frame(width: 20, height: 20)
  391. if isSelected { Rectangle().fill(cardColors.background).frame(width: 12, height: 12) }
  392. }.padding(.trailing, 5)
  393. }
  394. .padding(.horizontal, 20).padding(.vertical, 15)
  395. }
  396. .frame(height: 90)
  397. }
  398. .buttonStyle(PlainButtonStyle())
  399. }
  400. }
  401.  
  402. struct JapaneseBrutalistView_Previews: PreviewProvider {
  403. static var previews: some View {
  404. JapaneseBrutalistView()
  405. }
  406. }
  407.  
Comments
Add Comment
Please, Sign In to add comment