Flowing fields algorithmic art using SwiftUI
Inspired by Keith Peters and his two articles from 2017 about the creation of algorithmic art using flowing field into Java Script and HTML5, it made me wonder if it was possible to do the same using SwiftUI framework.
Well, anything that fits into an interface can be represented as a View structure but is it easy as it sounds? There are many resemblances between the code shown by Keith on the first part of the article, to build a responsive environment using the canvas grid and its positions and generate angles based on a mathematical regime. But in Swift we are more likely to plot a single path that concatenates all the small lines that composes the pattern.
Game plan
Therefore, what do we want to do? We want to create a matrix grid — subdivided by some pre stablished resolution — and filled with random positions coordinates pairs (x,y) in which each one of them holds an angle calculated from a given equation.
As Swift do not accepts logical operations inside a view, it is important to build core functions that will load the results on the background of our app and be able to input the results where they are needed for drawing paths into the canvas.
Core functions
randomPos
For the first part, we should look for a function that returns us an array of arrays with tuple pairs of random positions. We can call it randomPos(), and define it as
func randomPos(width: CGFloat) -> [[(Int,Int)]] {
var posMatrix: [[(Int,Int)]] = [[]]
var x = 0
var y = 0
for _ in 0..<Int(width) {
var rowArray: [(Int,Int)] = []
for _ in 0..<Int(width) {
x = Int.random(in: 0..<Int(width))
y = Int.random(in: 0..<Int(width))
rowArray.append((x,y))
}
posMatrix.append(rowArray)
}
return posMatrix
}
So here we are creating a map of possible locations that we will send our path to anchor later. This randomness collaborates with getAngle(), above.
getAngle
Next, we will create getAngle() to put a randomPos value given by the function to create angle patterns (and use it later on to rotate the path created).
func getAngle(randomPosCoord: (Int,Int), angle: Double) -> Double {
let value = curlsFunc(randomPosCoord: (randomPosCoord.0, randomPosCoord.1))
return value
}
Where the curlsFunc() is just an example for flow field equation that takes the randomPos() coordinates and turn them into a value for the field at that point.
func curlsFunc(randomPosCoord: (Int,Int)) -> Double {
//Ideally you want to slide over scale and angle values to get different pattern shapes
scale = 0.004
angle = 2.0
let x = Double(randomPosCoord.0) * scale
let y = Double(randomPosCoord.1) * scale
let t = x + y
let result = sin(t)*cos(t) * angle * Double.pi
return result
}
rotateCenter
Now, the last function translates and rotates the center of the path so we do not lose reference while drawing the patterns into the View.
This is one of the minor differences between Java Script and Swift, here we do not have a fixed positions reference from the path to itself, but from the view.
So, to correct that, we use rotateCenter():
func rotateCenter(x: CGFloat, y: CGFloat, center: CGPoint, angleR: Double) -> CGPoint {
let transform = CGAffineTransform
.identity
.translatedBy(x: center.x, y: center.y)
.scaledBy(x: 1, y: 1)
.rotated(by: angleR)
return CGPoint(x: x, y: y).applying(transform)
}
pathway
Then, we are able to draw our path using another function for that, which we call pathway().
func pathway(randomPos: [[(Int,Int)]], res: Double, angle: Double) -> Path {
let path = Path { path in
for x in 0..<randomPos.count/Int(res) {
for y in 0..<randomPos[x].count/Int(res) {
path.move(to: translateCenter(x: CGFloat(randomPos[x][y].0), y: CGFloat(randomPos[x][y].1), center: CGPoint(x: 0, y: 0)))
let randomAngle = getAngle(randomPosCoord: (randomPos[x][y].0,randomPos[x][y].1), angle: angle)
//this is an example of path size, play with this
let deltaX = 20*cos(randomAngle)
let deltaY = 0
path.addLine(to: rotateCenter(x: deltaX, y: deltaY, center: CGPoint(x: randomPos[x][y].0, y: randomPos[x][y].1), angleR: randomAngle))
}
}
}
self.path = path
return path
}
To see the results, we can create a view that shows our path. For that, we define a Path()
@State var path: Path = Path()
that receives from pathway a drawn path when we declared
func pathway() -> Path {
...
self.path = path //atributes a drawn path to the @State variable
return path
}
and then we can put the results into a GeometryReader.
struct FlowField: View {
@State var path: Path = Path()
var body: some View {
GeometryReader { geometry in
let width = max(geometry.size.width, geometry.size.height)
//drawn path using pathway
path
.stroke(.black, lineWidth: 1)
.drawingGroup()
.background(.white)
}
}
}
To get more interesting patterns, you should change the function on the getAngle() for the value variable. Then, more random shapes come from noise functions such as Perlin Noise or Simplex Noise, which are given by SwiftNoiseGenerator package (check it out on: https://swiftpack.co/package/hirota1ro/swift-noise-generator). Then, you should get something like this: