Flowing fields algorithmic art using SwiftUI

Anderson Vulto
Academy@EldoradoCPS
4 min readFeb 20, 2024
Visual representation of a Flowing field through the 2D Perlin Noise pattern.
Visual representation of a Flowing field through the 2D Perlin Noise pattern.

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)

}
}
}
The result for the FlowField View with scale = 0.004 and angle = 2π

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:

Colorful flow field patterns created using Flowin app.
Colorful flow field patterns created using Flowin app.

Thank you!

--

--

Anderson Vulto
Academy@EldoradoCPS

Applied Mathematics @ Unicamp. UI/UX Designer @ Developer Academy Campinas and passionated about art.