Created
February 25, 2025 08:58
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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