Skip to content

Instantly share code, notes, and snippets.

@alexjlockwood
Last active December 1, 2024 15:37
Show Gist options
  • Save alexjlockwood/2c48849d4f4c8318f4dfdf90b970f190 to your computer and use it in GitHub Desktop.
Save alexjlockwood/2c48849d4f4c8318f4dfdf90b970f190 to your computer and use it in GitHub Desktop.
Ring of Circles animation written in SwiftUI
import SwiftUI
private let kNumDots = 16
private let kTau: CGFloat = 2 * .pi
private let kDotPeriod: CGFloat = 10
private let kWavePeriod: CGFloat = 10 / (4 * kTau)
private let kRingRadiusFactor: CGFloat = 0.35
private let kWaveRadiusFactor: CGFloat = 0.10
struct RingOfCircles: View {
var body: some View {
TimelineView(.animation) { timeline in
Canvas { context, size in
let size = min(size.width, size.height)
let ringRadius = size * kRingRadiusFactor
let waveRadius = size * kWaveRadiusFactor
let dotRadius = waveRadius / 4
let dotGap = dotRadius / 2
let center = CGPoint(x: size / 2, y: size / 2)
let millis = timeline.date.timeIntervalSince1970
// Draw dots below the ring.
for index in 0..<kNumDots {
drawDot(
index: index,
millis: millis,
below: true,
ringRadius: ringRadius,
waveRadius: waveRadius,
dotRadius: dotRadius,
dotGap: dotGap,
context: &context,
center: center
)
}
// Draw the ring.
drawRing(
context: &context,
center: center,
radius: ringRadius,
lineWidth: dotRadius + dotGap * 2,
color: .white
)
drawRing(
context: &context,
center: center,
radius: ringRadius,
lineWidth: dotRadius,
color: .black
)
// Draw dots above the ring.
for index in 0..<kNumDots {
drawDot(
index: index,
millis: millis,
below: false,
ringRadius: ringRadius,
waveRadius: waveRadius,
dotRadius: dotRadius,
dotGap: dotGap,
context: &context,
center: center
)
}
}
}
}
}
private func drawRing(
context: inout GraphicsContext,
center: CGPoint,
radius: CGFloat,
lineWidth: CGFloat,
color: Color
) {
context.stroke(
Path { path in
path.addArc(
center: center,
radius: radius,
startAngle: .zero,
endAngle: .degrees(360),
clockwise: false
)
},
with: .color(color),
lineWidth: lineWidth
)
}
private func drawDot(
index: Int,
millis: CGFloat,
below: Bool,
ringRadius: CGFloat,
waveRadius: CGFloat,
dotRadius: CGFloat,
dotGap: CGFloat,
context: inout GraphicsContext,
center: CGPoint
) {
let dotAngle = ((CGFloat(index) / CGFloat(kNumDots)) + (millis / -kDotPeriod)).truncatingRemainder(dividingBy: 1) * kTau
let waveAngle = (dotAngle + (millis / -kWavePeriod)).truncatingRemainder(dividingBy: kTau)
guard (cos(waveAngle) > 0) == below else {
// Skip drawing the dot if it should be rendered underneath the ring.
return
}
// Rotate and translate the dot to its correct location
// along the circumference of the ring.
let rotationTransform = CGAffineTransform(rotationAngle: dotAngle)
let translationTransform = CGAffineTransform(translationX: ringRadius + sin(waveAngle) * waveRadius, y: 0)
let dotTransform = translationTransform.concatenating(rotationTransform)
let dotCenter = CGPoint(x: center.x + dotTransform.tx, y: center.y + dotTransform.ty)
context.stroke(
Path(
ellipseIn: CGRect(
x: dotCenter.x - dotRadius,
y: dotCenter.y - dotRadius,
width: dotRadius * 2,
height: dotRadius * 2
)
),
with: .color(.white),
lineWidth: dotGap * 2
)
context.fill(
Path(
ellipseIn: CGRect(
x: dotCenter.x - dotRadius,
y: dotCenter.y - dotRadius,
width: dotRadius * 2,
height: dotRadius * 2
)
),
with: .color(.black)
)
}
#Preview("Ring of Circles") {
RingOfCircles()
.padding(48)
.aspectRatio(1, contentMode: .fit)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment