Skip to content

Instantly share code, notes, and snippets.

@evarilci
Created February 25, 2025 08:58
Show Gist options
  • Select an option

  • Save evarilci/0065b89225f5f14aefc32920b19d7263 to your computer and use it in GitHub Desktop.

Select an option

Save evarilci/0065b89225f5f14aefc32920b19d7263 to your computer and use it in GitHub Desktop.
Bouncing ball within the rotating hexagon in Swift and UIKit. It has been created by o3-mini and fine tuned by me.
import UIKit
class ViewController: UIViewController {
// The view that draws the hexagon outline.
let hexagonView = HexagonView()
// The ball view.
let ballView = UIView(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
// Dynamics
var animator: UIDynamicAnimator!
var gravityBehavior: UIGravityBehavior!
var collisionBehavior: UICollisionBehavior!
var ballBehavior: UIDynamicItemBehavior!
var pushBehavior: UIPushBehavior!
// CADisplayLink for updating rotation and boundaries.
var displayLink: CADisplayLink!
// Rotation speed (radians per second)
let rotationSpeed: CGFloat = .pi / 4 // 45° per second
// Current rotation angle.
var currentAngle: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
// Setup hexagonView in the center.
let hexagonSize: CGFloat = 300
hexagonView.frame = CGRect(x: 0, y: 0, width: hexagonSize, height: hexagonSize)
hexagonView.center = view.center
view.addSubview(hexagonView)
hexagonView.backgroundColor = .white
// Setup ball appearance.
ballView.backgroundColor = .red
ballView.layer.cornerRadius = ballView.frame.width / 2
// Place the ball slightly off-center so it eventually meets an edge.
ballView.center = CGPoint(x: hexagonView.center.x, y: hexagonView.center.y - 50)
view.addSubview(ballView)
// Setup dynamic animator.
animator = UIDynamicAnimator(referenceView: view)
// Gravity behavior.
gravityBehavior = UIGravityBehavior(items: [ballView])
gravityBehavior.magnitude = 1.0
animator.addBehavior(gravityBehavior)
// Collision behavior.
collisionBehavior = UICollisionBehavior(items: [ballView])
collisionBehavior.translatesReferenceBoundsIntoBoundary = false
animator.addBehavior(collisionBehavior)
// Ball behavior with high elasticity.
ballBehavior = UIDynamicItemBehavior(items: [ballView])
ballBehavior.elasticity = 1.0
ballBehavior.friction = 0.0
ballBehavior.resistance = 0
animator.addBehavior(ballBehavior)
// Initial push so the ball moves.
pushBehavior = UIPushBehavior(items: [ballView], mode: .instantaneous)
pushBehavior.angle = .pi / 4
pushBehavior.magnitude = 0.3
animator.addBehavior(pushBehavior)
// Start the display link.
displayLink = CADisplayLink(target: self, selector: #selector(updateAnimation))
displayLink.add(to: .main, forMode: .default)
}
@objc func updateAnimation(displayLink: CADisplayLink) {
let dt = CGFloat(displayLink.duration)
currentAngle += rotationSpeed * dt
// Apply rotation to the hexagon view.
hexagonView.transform = CGAffineTransform(rotationAngle: currentAngle)
updateCollisionBoundaries()
}
/// Computes collision boundaries by first calculating the hexagon’s vertices (matching the drawn hexagon)
/// then offsetting each edge inward by half the ball’s width using the inward normal.
func updateCollisionBoundaries() {
collisionBehavior.removeAllBoundaries()
let center = hexagonView.center
let drawnRadius = min(hexagonView.bounds.width, hexagonView.bounds.height) / 2 * 1.1
let adjustedRadius = drawnRadius - ballView.frame.width / 2 // inset by the ball's radius
var vertices = [CGPoint]()
for i in 0..<6 {
let angle = CGFloat(i) * (2 * .pi / 6) - .pi / 2 + currentAngle
let vertex = CGPoint(x: center.x + adjustedRadius * cos(angle),
y: center.y + adjustedRadius * sin(angle))
vertices.append(vertex)
}
for i in 0..<vertices.count {
let start = vertices[i]
let end = vertices[(i + 1) % vertices.count]
collisionBehavior.addBoundary(withIdentifier: "\(i)" as NSString, from: start, to: end)
}
}
}
// Custom view that draws a hexagon.
class HexagonView: UIView {
override func draw(_ rect: CGRect) {
let path = hexagonPath()
UIColor.blue.setStroke()
path.lineWidth = 3.0
path.stroke()
}
/// Generates a hexagon path that fits in the view's bounds.
func hexagonPath() -> UIBezierPath {
let path = UIBezierPath()
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(bounds.width, bounds.height) / 2 * 0.9
for i in 0..<6 {
let angle = CGFloat(i) * (2 * .pi / 6) - .pi / 2
let point = CGPoint(x: center.x + radius * cos(angle),
y: center.y + radius * sin(angle))
if i == 0 {
path.move(to: point)
} else {
path.addLine(to: point)
}
}
path.close()
return path
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment