Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- //made for r/keisen
- //join to get more tutorials and neat design
- //have fun playing!
- import SwiftUI
- struct ColoredPoint: Identifiable {
- let id = UUID()
- let point: CGPoint
- let color: Color
- }
- // deboucnr stops the app from recalculating points like crazy
- // every time a slider moves a tiny bit, waits short moment
- class Debouncer {
- private let delay: TimeInterval
- private var workItem: DispatchWorkItem?
- init(delay: TimeInterval) {
- self.delay = delay
- }
- func debounce(action: @escaping () -> Void) {
- workItem?.cancel()
- let newWorkItem = DispatchWorkItem(block: action)
- workItem = newWorkItem
- //schedule on main queue directly as it involves UI updates
- DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: newWorkItem)
- }
- }
- struct CliffordAttractorView: View {
- // These are the magic numbers (a, b, c, d) that define the attractor's shape.
- @State private var a: Double = -1.4
- @State private var b: Double = 1.6
- @State private var c: Double = 1.0
- @State private var d: Double = 0.7
- // State for how the points look
- @State private var points: [ColoredPoint] = []
- @State private var numPoints: Int = 30000
- @State private var pointSize: CGFloat = 1.0
- @State private var scale: CGFloat = 150.0
- @State private var jitterAmount: CGFloat = 0.6
- @State private var pointOpacity: Double = 0.35
- @State private var parameterChangeDebouncer = Debouncer(delay: 0.05)
- var body: some View {
- VStack(spacing: 0) {
- Canvas { context, size in
- let centerX = size.width / 2.0
- let centerY = size.height / 2.0
- context.translateBy(x: centerX, y: centerY)
- // scale and jitter make it look cooler.
- for p in points {
- let screenX = p.point.x * scale
- let screenY = p.point.y * scale
- let jitterX = CGFloat.random(in: -jitterAmount...jitterAmount)
- let jitterY = CGFloat.random(in: -jitterAmount...jitterAmount)
- let rect = CGRect(x: screenX + jitterX - pointSize / 2.0,
- y: screenY + jitterY - pointSize / 2.0,
- width: pointSize,
- height: pointSize)
- context.fill(Path(rect), with: .color(p.color.opacity(pointOpacity)))
- }
- }
- .background(Color.black)
- .gesture(
- MagnificationGesture()
- .onChanged { value in
- scale = max(10, scale * value.magnitude)
- }
- )
- ControlPanelView(a: $a, b: $b, c: $c, d: $d, scale: $scale)
- }
- .ignoresSafeArea(.container, edges: .top) // Ignore top safe area for canvas
- .onChange(of: a) { _ in triggerRegeneration() }
- .onChange(of: b) { _ in triggerRegeneration() }
- .onChange(of: c) { _ in triggerRegeneration() }
- .onChange(of: d) { _ in triggerRegeneration() }
- .onAppear(perform: generatePoints)
- .preferredColorScheme(.dark)
- }
- func triggerRegeneration() {
- parameterChangeDebouncer.debounce {
- generatePoints()
- }
- }
- func generatePoints() {
- // this heavy lifting (calculating points) happens off the main thread
- DispatchQueue.global(qos: .userInitiated).async {
- var newPoints: [ColoredPoint] = []
- newPoints.reserveCapacity(numPoints)
- var x: Double = 0.1
- var y: Double = 0.1
- // core math loop for the Clifford Attractor
- for i in 0..<numPoints {
- let nextX = sin(a * y) + c * cos(a * x)
- let nextY = sin(b * x) + d * cos(b * y)
- x = nextX
- y = nextY
- if i < 50 { continue }
- // calculate color based on position (angle and distance from center)
- let angle = atan2(y, x)
- let radius = sqrt(x * x + y * y)
- let hue = (angle + .pi) / (2.0 * .pi)
- let maxRadius: Double = 2.0 // Helps scale brightness
- let brightness = min(1.0, 0.5 + (radius / maxRadius) * 0.5)
- let color = Color(hue: hue, saturation: 1.0, brightness: brightness)
- newPoints.append(ColoredPoint(point: CGPoint(x: x, y: y), color: color))
- }
- // once done calculating, jump back to the main thread to update the UI
- DispatchQueue.main.async {
- self.points = newPoints
- }
- }
- }
- }
- struct ControlPanelView: View {
- @Binding var a: Double
- @Binding var b: Double
- @Binding var c: Double
- @Binding var d: Double
- @Binding var scale: CGFloat
- let paramRange: ClosedRange<Double> = -1.6...1.6
- var body: some View {
- VStack(spacing: 8) {
- ParameterSlider(label: "a", value: $a, range: paramRange)
- ParameterSlider(label: "b", value: $b, range: paramRange)
- ParameterSlider(label: "c", value: $c, range: paramRange)
- ParameterSlider(label: "d", value: $d, range: paramRange)
- ParameterSlider(label: "Zoom", value: $scale, range: 10...800, step: 1)
- }
- .padding(.horizontal)
- .padding(.vertical, 15)
- .background(Color.black.opacity(0.6))
- }
- }
- struct ParameterSlider<V>: View where V: BinaryFloatingPoint, V.Stride: BinaryFloatingPoint {
- let label: String
- @Binding var value: V
- let range: ClosedRange<V>
- var step: V.Stride? = nil
- var body: some View {
- HStack {
- Text(label)
- .font(.system(size: 14))
- .foregroundColor(.white.opacity(0.8))
- .frame(width: 45, alignment: .leading)
- if let actualStep = step {
- Slider(value: $value, in: range, step: actualStep)
- } else {
- Slider(value: $value, in: range)
- }
- Text(String(format: "%.3f", Double(value)))
- .font(.system(size: 12, weight: .regular, design: .monospaced))
- .foregroundColor(.white.opacity(0.7))
- .frame(width: 55, alignment: .trailing)
- }
- .frame(height: 30)
- }
- }
- struct CliffordAttractorView_Previews: PreviewProvider {
- static var previews: some View {
- CliffordAttractorView()
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement