Skip to content

Instantly share code, notes, and snippets.

@DominatorVbN
Created October 1, 2023 09:25
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DominatorVbN/7bb77efc1b2a44f2214444562113ed87 to your computer and use it in GitHub Desktop.
Save DominatorVbN/7bb77efc1b2a44f2214444562113ed87 to your computer and use it in GitHub Desktop.
Simulation of mathematical concept of Chaos Game
import SwiftUI
import SwiftData
// TODO: Make this animatable shape
struct Polygon: Shape {
var edges: Int
var pathUpdated: (Path) -> Void
var vertexUpdated:([CGPoint]) -> Void
func path(in rect: CGRect) -> Path {
let path = Path { path in
let center = CGPoint(x: rect.size.width / 2, y: rect.size.height / 2)
let radius = min(rect.size.width, rect.size.height) / 2
let angle = CGFloat.pi * 2 / CGFloat(max(edges,1))
let firstVertexLocation = CGPoint(x: center.x + radius, y: center.y)
path.move(to: firstVertexLocation)
var vertexLocations: [CGPoint] = [firstVertexLocation]
for index in 1...max(edges,1) {
let x = center.x + radius * cos(angle * CGFloat(index))
let y = center.y + radius * sin(angle * CGFloat(index))
let location = CGPoint(x: x, y: y)
vertexLocations.append(location)
path.addLine(to: location)
}
vertexUpdated(vertexLocations)
}
pathUpdated(path)
return path
}
}
struct ContentView: View {
enum ShapeState: Equatable {
case idle
case simulating
}
@State var edges: Int = 3
@State var state: ShapeState = .idle
@State var path: Path = Path()
@State var vertexLocations: [CGPoint] = []
@State var isRandomLocationSelected: Bool = false
@State var lastDotLocation: CGPoint = .zero
@State var dotLocations: [CGPoint] = []
@State var timer = Timer.publish(every: 0.0000001, on: .main, in: .common).autoconnect()
var body: some View {
ZStack {
Color.black.ignoresSafeArea(.all)
VStack {
ZStack {
Polygon(edges: edges) {
self.path = $0
} vertexUpdated: {
vertexLocations = $0
}
.stroke(Color.white, lineWidth: 2)
ForEach(dotLocations, id: \.self) { location in
Circle()
.frame(width: 2, height: 2)
.foregroundColor(.white)
.position(location)
}
}
Button(action: {
switch state {
case .idle:
selectRandomPoint()
state = .simulating
case .simulating:
state = .idle
}
}, label: {
switch state {
case .idle:
Text("Start Simulation")
case .simulating:
Text("Stop Simulation")
}
})
.buttonStyle(.borderedProminent)
}
}
.onReceive(timer, perform: { firedDate in
guard state == .simulating else {
return
}
let randomVertex = vertexLocations.randomElement()!
let newLocation = CGPoint(x: (lastDotLocation.x + randomVertex.x) / 2, y: (lastDotLocation.y + randomVertex.y) / 2)
lastDotLocation = newLocation
dotLocations.append(newLocation)
})
}
// Not the best solution but works as a POC
func selectRandomPoint() {
guard isRandomLocationSelected == false else {
return
}
// Create a CGPoint which should be inside the path
// Add it to dotLocations
var randomPoint: CGPoint?
while randomPoint == nil {
// Generate a random point inside the path bounds
let randomX = CGFloat.random(in: UIScreen.main.bounds.minX...UIScreen.main.bounds.maxX)
let randomY = CGFloat.random(in: UIScreen.main.bounds.minY...UIScreen.main.bounds.maxY)
let potentialPoint = CGPoint(x: randomX, y: randomY)
// Check if the potential point is inside the path
if path.contains(potentialPoint) {
// If it is inside, assign it to randomPoint and exit the loop
randomPoint = potentialPoint
break
}
}
if let randomPoint = randomPoint {
dotLocations.append(randomPoint)
lastDotLocation = randomPoint
isRandomLocationSelected = true
}
}
}
#Preview {
ContentView()
}
extension CGPoint: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(x)
hasher.combine(y)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment