Created
November 21, 2020 23:35
-
-
Save Orangetronic/a028602a7336ba465625a19399225d41 to your computer and use it in GitHub Desktop.
Dump this in an iOS playground, and click around to simulate what would be index finger point tracking
This file contains 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
//: A UIKit based Playground for presenting user interface | |
import UIKit | |
import PlaygroundSupport | |
import CoreGraphics | |
import simd | |
/// # Some Convencience Extensions | |
extension simd_float2 { | |
func clampLength (_ f: Float) -> simd_float2 { | |
if simd_length(self) < f { return self } | |
if f == 0 { return simd_make_float2(0, 0) } | |
return simd_normalize(self) * f | |
} | |
} | |
extension CGPoint { | |
init (_ vector: simd_float2) { | |
self = CGPoint( | |
x: CGFloat(vector.x), | |
y: CGFloat(vector.y) | |
) | |
} | |
func to_simd_float2() -> simd_float2 { | |
return simd_make_float2( | |
Float(self.x), | |
Float(self.y) | |
) | |
} | |
} | |
/// # Laws of the universe | |
fileprivate let TERMINAL_VELOCITY: Float = 2 | |
fileprivate let MAX_G: Float = 0.2 | |
/// # A lil circle class with a colour, radius, and position | |
class UICircle { | |
var shapeLayer = CAShapeLayer() | |
var position: simd_float2 | |
var radius: CGFloat | |
init (center: CGPoint, radius: CGFloat) { | |
self.position = center.to_simd_float2() | |
self.radius = radius | |
setPath() | |
self.shapeLayer.fillColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1).cgColor | |
} | |
@discardableResult | |
func setColor(_ col: CGColor) -> Self { | |
self.shapeLayer.fillColor = col | |
self.shapeLayer.strokeColor = col | |
return self | |
} | |
func setPath () { | |
let circle = UIBezierPath( | |
arcCenter: CGPoint(position), | |
radius: radius, | |
startAngle: 0, | |
endAngle: CGFloat.pi * 2, | |
clockwise: true | |
) | |
self.shapeLayer.path = circle.cgPath | |
} | |
} | |
/// # A circle whom is a cursor | |
class UICircleCursor: UICircle { | |
var targetSize: CGFloat | |
var V: simd_float2 | |
var target: simd_float2 | |
override init (center: CGPoint, radius: CGFloat) { | |
targetSize = radius | |
self.V = simd_make_float2(0, 0) | |
self.target = center.to_simd_float2() | |
super.init(center: center, radius: radius) | |
} | |
func setTarget(_ pos: CGPoint) { | |
self.target = pos.to_simd_float2() | |
} | |
func tick() { | |
self.position = self.position + self.V | |
if simd_distance_squared(self.target, self.position) < 20 { | |
self.position = self.target | |
} | |
self.setPath() | |
let toTarget = self.target - self.position | |
let dV = toTarget.clampLength(MAX_G) // acceleration towards target | |
let distanceToTarget = simd_length(toTarget) | |
var TV = distanceToTarget < 2 * TERMINAL_VELOCITY | |
? distanceToTarget / 2 | |
: TERMINAL_VELOCITY | |
if TV < 1 { TV = 1 } | |
self.V = (self.V + dV).clampLength(TV) | |
} | |
} | |
class UICircleTarget: UICircle { | |
enum TargetStatus { | |
case inactive | |
case active(Int) | |
} | |
var width: CGFloat = 10.0 | |
var action: (() -> ())? = nil | |
var threshold: Int = 60 | |
var status: TargetStatus = .inactive | |
var indicator: UICircle | |
var maskLayer = CAShapeLayer() | |
override init (center: CGPoint, radius: CGFloat) { | |
self.indicator = UICircle(center: center, radius: 10).setColor(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 0).cgColor) | |
super.init(center: center, radius: radius) | |
self.shapeLayer.fillColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0).cgColor | |
self.shapeLayer.strokeColor = #colorLiteral(red: 0.8549019694, green: 0.250980407, blue: 0.4784313738, alpha: 1).cgColor | |
self.shapeLayer.lineWidth = width | |
self.shapeLayer.addSublayer(self.indicator.shapeLayer) | |
self.maskLayer.path = self.shapeLayer.path!.copy() | |
self.indicator.shapeLayer.mask = self.maskLayer | |
} | |
override func setColor(_ col: CGColor) -> Self { | |
super.setColor(col) | |
self.shapeLayer.fillColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0).cgColor | |
return self | |
} | |
func contains (_ cursor: UICircleCursor) -> Bool { | |
let distance_squared = simd_distance_squared(self.position, cursor.position) | |
let size_difference = Float(self.radius - cursor.radius) | |
let size_diff_squared = size_difference * size_difference | |
let isOutside = (size_diff_squared < distance_squared) && (self.radius > cursor.radius) | |
let contains = !isOutside | |
return contains | |
} | |
func tick (_ cursor: UICircleCursor) { | |
if !self.contains(cursor) { | |
self.status = .inactive | |
self.indicator.setColor(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 0).cgColor) | |
} else { | |
var nextCount = 1 | |
switch self.status { | |
case .inactive: | |
self.indicator.setColor(self.shapeLayer.strokeColor!) | |
self.status = .active(nextCount) | |
case .active(let currentCount): | |
nextCount = currentCount + 1 | |
} | |
self.status = .active(nextCount) | |
self.indicator.position = cursor.position | |
self.indicator.radius = CGFloat(nextCount + 10) | |
self.indicator.setPath() | |
if nextCount == threshold { | |
if let action = self.action { | |
action() | |
} | |
} | |
} | |
} | |
func setAction (_ closure: @escaping () -> ()) { | |
self.action = closure | |
} | |
} | |
class MyViewController : UIViewController { | |
var cursor: UICircleCursor = UICircleCursor( | |
center: CGPoint(x: 100, y: 100), | |
radius: 15) | |
.setColor(#colorLiteral(red: 0.8549019694, green: 0.250980407, blue: 0.4784313738, alpha: 1).cgColor) | |
var target: UICircleTarget = UICircleTarget( | |
center: CGPoint(x: 200, y: 200), | |
radius: 50) | |
.setColor(#colorLiteral(red: 0.8549019694, green: 0.250980407, blue: 0.4784313738, alpha: 1).cgColor) | |
var ticker: Timer? = nil | |
override func loadView() { | |
let view = UIView() | |
view.backgroundColor = .white | |
self.view = view | |
view.layer.addSublayer(cursor.shapeLayer) | |
view.layer.addSublayer(target.shapeLayer) | |
let tap = UITapGestureRecognizer(target: self, action: #selector(tapped(sender:))) | |
view.addGestureRecognizer(tap) | |
ticker = Timer.scheduledTimer(withTimeInterval: 0.015, repeats: true, block: { (t) in | |
self.cursor.tick() | |
self.target.tick(self.cursor) | |
}) | |
} | |
override func viewWillDisappear(_ animated: Bool) { | |
self.ticker?.invalidate() | |
self.ticker = nil | |
} | |
@objc func tapped (sender: UITapGestureRecognizer) { | |
let pos = sender.location(in: self.view) | |
self.moved(pos) | |
} | |
func moved (_ pos: CGPoint) { | |
self.cursor.setTarget(pos) | |
} | |
} | |
// Present the view controller in the Live View window | |
PlaygroundPage.current.liveView = MyViewController() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment