A tutorial on how to create a spirograph using SwiftUI

A tutorial on how to create a spirograph using SwiftUI

[[412896]]

This article is reprinted from the WeChat public account "Swift Community", written by Wei Xian Zhy. Please contact the Swift Community public account to reprint this article.

To get some serious drawing done, I’ll walk you through creating a simple spirograph with SwiftUI. A “Spirograph” is the brand name for a toy where you place a pencil in a circle and spin it around the circumference of another circle to create various geometric patterns, called roulette — just like the casino game.

This code contains a very specific formula. I’ll explain it, but it’s totally fine to skip this section if you’re not interested — this is just for fun, and there’s nothing new about Swift or SwiftUI here.

Our algorithm has four inputs:

  • The radius of the inner circle.
  • The radius of the outer ring.
  • The distance between the virtual pen and the center of the outer circle.
  • How many roulette wheels to draw. This is optional, but I think it really helps to show what's going on when the algorithm is working.

So let’s get started:

  1. struct Spirograph: Shape {
  2. let innerRadius: Int  
  3. let outerRadius: Int  
  4. let distance: Int  
  5. let amount: CGFloat
  6. }

We then prepare three values ​​from the data, starting with the greatest common divisor (GCD) of the inner and outer radii. Calculating the GCD of two numbers is usually done using Euclid's algorithm, which takes a slightly simplified form as follows:

  1. func gcd(_ a: Int , _ b: Int ) -> Int {
  2. var a = a
  3. var b = b
  4. while b != 0 {
  5. let temp = b
  6. b = a % b
  7. a = temp  
  8. }
  9. return a
  10. }

Add this method to the Spirograph structure.

The other two values ​​are the difference between the inner radius and the outer radius, and how many steps we need to perform to draw the roulette wheel - this is 360 degrees times the outer radius divided by the greatest common divisor, times our quantity input. All of our inputs work best when they are provided as integers, but when drawing the roulette wheel we need to use CGFloat, so we'll also create CGFloat copies of our inputs.

Now add this path(in:) method to the Spirograph structure:

  1. func path( in rect: CGRect) -> Path {
  2. let divisor = gcd(innerRadius, outerRadius)
  3. let outerRadius = CGFloat(self.outerRadius)
  4. let innerRadius = CGFloat(self.innerRadius)
  5. let distance = CGFloat(self.distance)
  6. let difference = innerRadius - outerRadius
  7. let endPoint = ceil(2 * CGFloat.pi * outerRadius / CGFloat(divisor)) * amount
  8.  
  9. // more code to come
  10. }

Finally, we can draw the roulette wheel by looping from 0 to our end point, and placing the point at the exact X/Y coordinate. Calculating the X/Y coordinates of a given point in the loop (called "theta") is where the real math comes in, but to be honest, I just converted the standard equation from Wikipedia into Swift - it's not something I dream of memorizing!

  • X is equal to the radius difference multiplied by the cosine of θ, multiplied by the cosine of the radius difference divided by the distance of the outer radius multiplied by θ.
  • Y is equal to the radius difference times the sine of θ, minus the distance times the sine of the radius difference divided by the outer radius times θ.

This is the core algorithm, but we’re going to make two small changes: we’re going to add half the width or height of the drawing rectangle to X and Y, respectively, to center it in drawing space; and if θ is 0 — that is, if this is the first point drawn in the wheel — we’re going to call move(to:) instead of addLine(to:) on our path.

Here’s the final code for the path(in:) method—replace the // more code to come comment with the following:

  1. var path = Path()
  2.  
  3. for theta in stride( from : 0, through: endPoint, by : 0.01) {
  4. var x = difference * cos(theta) + distance * cos(difference / outerRadius * theta)
  5. var y = difference * sin(theta) - distance * sin(difference / outerRadius * theta)
  6.  
  7. x += rect.width / 2
  8. y += rect.height / 2
  9.  
  10. if theta == 0 {
  11. path. move ( to : CGPoint(x: x, y: y))
  12. } else {
  13. path.addLine( to : CGPoint(x: x, y: y))
  14. }
  15. }
  16.  
  17. return path

I realize this is a lot of heavy math, but the payoff is coming: we can now use this shape in a view, adding various sliders to control the inner radius, outer radius, distance, amount, and even color:

  1. struct ContentView: View {
  2. @State private var innerRadius = 125.0
  3. @State private var outerRadius = 75.0
  4. @State private var distance = 25.0
  5. @State private var amount: CGFloat = 1.0
  6. @State private var hue = 0.6
  7.  
  8. var body: some   View {
  9. VStack(spacing: 0) {
  10. Spacer()
  11.  
  12. Spirograph(innerRadius: Int (innerRadius), outerRadius: Int (outerRadius), distance: Int (distance), amount: amount)
  13. .stroke(Color(hue: hue, saturation: 1, brightness: 1), lineWidth: 1)
  14. .frame(width: 300, height: 300)
  15.  
  16. Spacer()
  17.  
  18. Group {
  19. Text( "Inner radius: \(Int(innerRadius))" )
  20. Slider(value: $innerRadius, in : 10...150, step: 1)
  21. .padding([.horizontal, .bottom])
  22.  
  23. Text( "Outer radius: \(Int(outerRadius))" )
  24. Slider(value: $outerRadius, in : 10...150, step: 1)
  25. .padding([.horizontal, .bottom])
  26.  
  27. Text( "Distance: \(Int(distance))" )
  28. Slider(value: $distance, in : 1...150, step: 1)
  29. .padding([.horizontal, .bottom])
  30.  
  31. Text( "Amount: \(amount, specifier: " %.2f ")" )
  32. Slider(value: $amount)
  33. .padding([.horizontal, .bottom])
  34.  
  35. Text( "Color" )
  36. Slider(value: $hue)
  37. .padding(.horizontal)
  38. }
  39. }
  40. }
  41. }

That’s a lot of code, but I hope you take the time to run the app and appreciate how beautiful the roulette wheel is. What you’re seeing is actually just a form of roulette, known as a hypotrochoid — with small tweaks to the algorithm, you can generate epitrochoids and so on, which are beautiful in different ways.

Before I sign off, I want to remind you that the parametric equations used here are standard in mathematics, not something I just invented - I actually went to Baidu and looked up the page about hypotrochoids[1] and converted them into Swift.

References

[1]hypotrochoids: http://www.durangobill.com/Trochoids.html

<<:  The first batch of user reviews for iOS 14.7 are out, bringing 3 bad news, and it is recommended not to update for the time being

>>:  Finally supports multiple devices online at the same time! Detailed experience of the new version of WeChat

Recommend

Can correct kneeling method cure the disease caused by sitting out?

Recently, the editor has been complaining about b...

How did the ancients prevent fire during the Qin and Han dynasties?

The Qin and Han dynasties are collectively known ...

New Oriental F2020 General Doctoral English Full Course Baidu Cloud Download

New Oriental F2020 General Doctoral English Full ...

"Get started first" and you have eliminated more than 50% of your competitors

A famous domestic novelist came to the school to ...

Bilibili Operation: How to improve user loyalty?

With its content quality, community atmosphere, u...

A Chinese man installed a pirated version of Windows 7 and was fined 10,000 yuan

Are you using a pirated Windows system? Then be c...

Thermos cup turns into a "bomb"? You must know these safety hazards →

In daily life Thermos cup is convenient and has h...

Perfect Diary’s community operation method!

1. Introduction I will not go into details about ...

From community to e-commerce, how did Xiaohongshu become popular?

Since its establishment in June 2013, Xiaohongshu...