Advertisement
walkerbuildapps

clifford chaos!

Apr 12th, 2025
113
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 6.59 KB | None | 0 0
  1. //made for r/keisen
  2. //join to get more tutorials and neat design
  3.  
  4. //have fun playing!
  5.  
  6. import SwiftUI
  7.  
  8. struct ColoredPoint: Identifiable {
  9. let id = UUID()
  10. let point: CGPoint
  11. let color: Color
  12. }
  13.  
  14. // deboucnr stops the app from recalculating points like crazy
  15. // every time a slider moves a tiny bit, waits short moment
  16. class Debouncer {
  17. private let delay: TimeInterval
  18. private var workItem: DispatchWorkItem?
  19.  
  20. init(delay: TimeInterval) {
  21. self.delay = delay
  22. }
  23.  
  24. func debounce(action: @escaping () -> Void) {
  25. workItem?.cancel()
  26. let newWorkItem = DispatchWorkItem(block: action)
  27. workItem = newWorkItem
  28. //schedule on main queue directly as it involves UI updates
  29. DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: newWorkItem)
  30. }
  31. }
  32.  
  33. struct CliffordAttractorView: View {
  34. // These are the magic numbers (a, b, c, d) that define the attractor's shape.
  35. @State private var a: Double = -1.4
  36. @State private var b: Double = 1.6
  37. @State private var c: Double = 1.0
  38. @State private var d: Double = 0.7
  39.  
  40. // State for how the points look
  41. @State private var points: [ColoredPoint] = []
  42. @State private var numPoints: Int = 30000
  43. @State private var pointSize: CGFloat = 1.0
  44. @State private var scale: CGFloat = 150.0
  45. @State private var jitterAmount: CGFloat = 0.6
  46. @State private var pointOpacity: Double = 0.35
  47. @State private var parameterChangeDebouncer = Debouncer(delay: 0.05)
  48.  
  49.  
  50. var body: some View {
  51. VStack(spacing: 0) {
  52. Canvas { context, size in
  53. let centerX = size.width / 2.0
  54. let centerY = size.height / 2.0
  55. context.translateBy(x: centerX, y: centerY)
  56.  
  57. // scale and jitter make it look cooler.
  58. for p in points {
  59. let screenX = p.point.x * scale
  60. let screenY = p.point.y * scale
  61. let jitterX = CGFloat.random(in: -jitterAmount...jitterAmount)
  62. let jitterY = CGFloat.random(in: -jitterAmount...jitterAmount)
  63.  
  64. let rect = CGRect(x: screenX + jitterX - pointSize / 2.0,
  65. y: screenY + jitterY - pointSize / 2.0,
  66. width: pointSize,
  67. height: pointSize)
  68.  
  69. context.fill(Path(rect), with: .color(p.color.opacity(pointOpacity)))
  70. }
  71. }
  72. .background(Color.black)
  73. .gesture(
  74. MagnificationGesture()
  75. .onChanged { value in
  76. scale = max(10, scale * value.magnitude)
  77. }
  78. )
  79.  
  80. ControlPanelView(a: $a, b: $b, c: $c, d: $d, scale: $scale)
  81.  
  82. }
  83. .ignoresSafeArea(.container, edges: .top) // Ignore top safe area for canvas
  84. .onChange(of: a) { _ in triggerRegeneration() }
  85. .onChange(of: b) { _ in triggerRegeneration() }
  86. .onChange(of: c) { _ in triggerRegeneration() }
  87. .onChange(of: d) { _ in triggerRegeneration() }
  88. .onAppear(perform: generatePoints)
  89. .preferredColorScheme(.dark)
  90. }
  91.  
  92. func triggerRegeneration() {
  93. parameterChangeDebouncer.debounce {
  94. generatePoints()
  95. }
  96. }
  97.  
  98. func generatePoints() {
  99. // this heavy lifting (calculating points) happens off the main thread
  100. DispatchQueue.global(qos: .userInitiated).async {
  101. var newPoints: [ColoredPoint] = []
  102. newPoints.reserveCapacity(numPoints)
  103. var x: Double = 0.1
  104. var y: Double = 0.1
  105.  
  106. // core math loop for the Clifford Attractor
  107. for i in 0..<numPoints {
  108. let nextX = sin(a * y) + c * cos(a * x)
  109. let nextY = sin(b * x) + d * cos(b * y)
  110. x = nextX
  111. y = nextY
  112.  
  113. if i < 50 { continue }
  114.  
  115. // calculate color based on position (angle and distance from center)
  116. let angle = atan2(y, x)
  117. let radius = sqrt(x * x + y * y)
  118. let hue = (angle + .pi) / (2.0 * .pi)
  119. let maxRadius: Double = 2.0 // Helps scale brightness
  120. let brightness = min(1.0, 0.5 + (radius / maxRadius) * 0.5)
  121. let color = Color(hue: hue, saturation: 1.0, brightness: brightness)
  122.  
  123. newPoints.append(ColoredPoint(point: CGPoint(x: x, y: y), color: color))
  124. }
  125.  
  126. // once done calculating, jump back to the main thread to update the UI
  127. DispatchQueue.main.async {
  128. self.points = newPoints
  129. }
  130. }
  131. }
  132. }
  133.  
  134.  
  135. struct ControlPanelView: View {
  136. @Binding var a: Double
  137. @Binding var b: Double
  138. @Binding var c: Double
  139. @Binding var d: Double
  140. @Binding var scale: CGFloat
  141.  
  142. let paramRange: ClosedRange<Double> = -1.6...1.6
  143.  
  144. var body: some View {
  145. VStack(spacing: 8) {
  146. ParameterSlider(label: "a", value: $a, range: paramRange)
  147. ParameterSlider(label: "b", value: $b, range: paramRange)
  148. ParameterSlider(label: "c", value: $c, range: paramRange)
  149. ParameterSlider(label: "d", value: $d, range: paramRange)
  150. ParameterSlider(label: "Zoom", value: $scale, range: 10...800, step: 1)
  151. }
  152. .padding(.horizontal)
  153. .padding(.vertical, 15)
  154. .background(Color.black.opacity(0.6))
  155. }
  156. }
  157.  
  158.  
  159. struct ParameterSlider<V>: View where V: BinaryFloatingPoint, V.Stride: BinaryFloatingPoint {
  160. let label: String
  161. @Binding var value: V
  162. let range: ClosedRange<V>
  163. var step: V.Stride? = nil
  164.  
  165. var body: some View {
  166. HStack {
  167. Text(label)
  168. .font(.system(size: 14))
  169. .foregroundColor(.white.opacity(0.8))
  170. .frame(width: 45, alignment: .leading)
  171.  
  172. if let actualStep = step {
  173. Slider(value: $value, in: range, step: actualStep)
  174. } else {
  175. Slider(value: $value, in: range)
  176. }
  177.  
  178. Text(String(format: "%.3f", Double(value)))
  179. .font(.system(size: 12, weight: .regular, design: .monospaced))
  180. .foregroundColor(.white.opacity(0.7))
  181. .frame(width: 55, alignment: .trailing)
  182. }
  183. .frame(height: 30)
  184. }
  185. }
  186.  
  187.  
  188. struct CliffordAttractorView_Previews: PreviewProvider {
  189. static var previews: some View {
  190. CliffordAttractorView()
  191. }
  192. }
  193.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement