2021-10-12 15:07:00 +04:00

540 lines
16 KiB
Swift

//
// MathKit.swift
// UIToolBox
//
// Created by Brandon Withrow on 10/10/18.
//
// From https://github.com/buba447/UIToolBox
import Foundation
import CoreGraphics
extension Int {
var cgFloat: CGFloat {
return CGFloat(self)
}
}
extension Double {
var cgFloat: CGFloat {
return CGFloat(self)
}
}
extension CGFloat: Interpolatable {
/**
Interpolates the receiver to the given number by Amount.
- Parameter toNumber: The number to interpolate to.
- Parameter amount: The amount to interpolate from 0-1
```
let number = 5
let interpolated = number.interpolateTo(10, amount: 0.5)
print(interpolated)
// Result: 7.5
```
1. The amount can be greater than one and less than zero. The interpolation will not be clipped.
*/
func interpolateTo(_ to: CGFloat, amount: CGFloat) -> CGFloat {
return self + ((to - self) * CGFloat(amount))
}
func interpolateTo(_ to: CGFloat, amount: CGFloat, spatialOutTangent: CGPoint?, spatialInTangent: CGPoint?) -> CGFloat {
return interpolateTo(to, amount: amount)
}
func remap(fromLow: CGFloat, fromHigh: CGFloat, toLow: CGFloat, toHigh: CGFloat) -> CGFloat {
guard (fromHigh - fromLow) != 0 else {
// Would produce NAN
return 0
}
return toLow + (self - fromLow) * (toHigh - toLow) / (fromHigh - fromLow)
}
/**
Returns a value that is clamped between the two numbers
1. The order of arguments does not matter.
*/
func clamp(_ a: CGFloat, _ b: CGFloat) -> CGFloat {
return CGFloat(Double(self).clamp(Double(a), Double(b)))
}
/**
Returns the difference between the receiver and the given number.
- Parameter absolute: If *true* (Default) the returned value will always be positive.
*/
func diff(_ a: CGFloat, absolute: Bool = true) -> CGFloat {
return absolute ? abs(a - self) : a - self
}
func toRadians() -> CGFloat { return self * .pi / 180 }
func toDegrees() -> CGFloat { return self * 180 / .pi }
}
extension Double: Interpolatable {
/**
Interpolates the receiver to the given number by Amount.
- Parameter toNumber: The number to interpolate to.
- Parameter amount: The amount to interpolate from 0-1
```
let number = 5
let interpolated = number.interpolateTo(10, amount: 0.5)
print(interpolated)
// Result: 7.5
```
1. The amount can be greater than one and less than zero. The interpolation will not be clipped.
*/
func interpolateTo(_ to: Double, amount: CGFloat) -> Double {
return self + ((to - self) * Double(amount))
}
func interpolateTo(_ to: Double, amount: CGFloat, spatialOutTangent: CGPoint?, spatialInTangent: CGPoint?) -> Double {
return interpolateTo(to, amount: amount)
}
func remap(fromLow: Double, fromHigh: Double, toLow: Double, toHigh: Double) -> Double {
return toLow + (self - fromLow) * (toHigh - toLow) / (fromHigh - fromLow)
}
/**
Returns a value that is clamped between the two numbers
1. The order of arguments does not matter.
*/
func clamp(_ a: Double, _ b: Double) -> Double {
let minValue = a <= b ? a : b
let maxValue = a <= b ? b : a
return max(min(self, maxValue), minValue)
}
}
extension CGRect {
/// Initializes a new CGRect with a center point and size.
init(center: CGPoint, size: CGSize) {
self.init(x: center.x - (size.width * 0.5),
y: center.y - (size.height * 0.5),
width: size.width,
height: size.height)
}
/// Returns the total area of the rect.
var area: CGFloat {
return width * height
}
/// The center point of the rect. Settable.
var center: CGPoint {
get {
return CGPoint(x: midX, y: midY)
}
set {
origin = CGPoint(x: newValue.x - (size.width * 0.5),
y: newValue.y - (size.height * 0.5))
}
}
/// The top left point of the rect. Settable.
var topLeft: CGPoint {
get {
return CGPoint(x: minX, y: minY)
}
set {
origin = CGPoint(x: newValue.x,
y: newValue.y)
}
}
/// The bottom left point of the rect. Settable.
var bottomLeft: CGPoint {
get {
return CGPoint(x: minX, y: maxY)
}
set {
origin = CGPoint(x: newValue.x,
y: newValue.y - size.height)
}
}
/// The top right point of the rect. Settable.
var topRight: CGPoint {
get {
return CGPoint(x: maxX, y: minY)
}
set {
origin = CGPoint(x: newValue.x - size.width,
y: newValue.y)
}
}
/// The bottom right point of the rect. Settable.
var bottomRight: CGPoint {
get {
return CGPoint(x: maxX, y: maxY)
}
set {
origin = CGPoint(x: newValue.x - size.width,
y: newValue.y - size.height)
}
}
/**
Interpolates the receiver to the given rect by Amount.
- Parameter to: The rect to interpolate to.
- Parameter amount: The amount to interpolate from 0-1
```
let rect = CGRect(x:0, y:0, width: 50, height: 50)
let interpolated = rect.interpolateTo(CGRect(x:100, y:100, width: 100, height: 100), amount: 0.5)
print(interpolated)
// Result: (x: 50, y: 50, width: 75, height: 75)
```
1. The amount can be greater than one and less than zero. The interpolation will not be clipped.
*/
func interpolateTo(_ to: CGRect, amount: CGFloat) -> CGRect {
return CGRect(x: origin.x.interpolateTo(to.origin.x, amount: amount),
y: origin.y.interpolateTo(to.origin.y, amount: amount),
width: width.interpolateTo(to.width, amount: amount),
height: height.interpolateTo(to.height, amount: amount))
}
}
extension CGSize {
/**
Interpolates the receiver to the given size by Amount.
- Parameter to: The size to interpolate to.
- Parameter amount: The amount to interpolate from 0-1
```
let size = CGSize(width: 50, height: 50)
let interpolated = rect.interpolateTo(CGSize(width: 100, height: 100), amount: 0.5)
print(interpolated)
// Result: (width: 75, height: 75)
```
1. The amount can be greater than one and less than zero. The interpolation will not be clipped.
*/
func interpolateTo(_ to: CGSize, amount: CGFloat) -> CGSize {
return CGSize(width: width.interpolateTo(to.width, amount: amount),
height: height.interpolateTo(to.height, amount: amount))
}
/// Returns the scale float that will fit the receive inside of the given size.
func scaleThatFits(_ size: CGSize) -> CGFloat {
return CGFloat.minimum(width / size.width, height / size.height)
}
/// Adds receiver size to give size.
func add(_ size: CGSize) -> CGSize {
return CGSize(width: width + size.width, height: height + size.height)
}
/// Subtracts given size from receiver size.
func subtract(_ size: CGSize) -> CGSize {
return CGSize(width: width - size.width, height: height - size.height)
}
/// Multiplies receiver size by the given size.
func multiply(_ size: CGSize) -> CGSize {
return CGSize(width: width * size.width, height: height * size.height)
}
/// Operator convenience to add sizes with +
static func +(left: CGSize, right: CGSize) -> CGSize {
return left.add(right)
}
/// Operator convenience to subtract sizes with -
static func -(left: CGSize, right: CGSize) -> CGSize {
return left.subtract(right)
}
/// Operator convenience to multiply sizes with *
static func *(left: CGSize, right: CGFloat) -> CGSize {
return CGSize(width: left.width * right, height: left.height * right)
}
}
/// A struct that defines a line segment with two CGPoints
struct CGLine {
/// The Start of the line segment.
var start: CGPoint
/// The End of the line segment.
var end: CGPoint
/// Initializes a line segment with start and end points
init(start: CGPoint, end: CGPoint) {
self.start = start
self.end = end
}
/// The length of the line segment.
var length: CGFloat {
return end.distanceTo(start)
}
/// Returns a line segment that is normalized to a length of 1
func normalize() -> CGLine {
let len = length
guard len > 0 else {
return self
}
let relativeEnd = end - start
let relativeVector = CGPoint(x: relativeEnd.x / len, y: relativeEnd.y / len)
let absoluteVector = relativeVector + start
return CGLine(start: start, end: absoluteVector)
}
/// Trims a line segment to the given length
func trimmedToLength(_ toLength: CGFloat) -> CGLine {
let len = length
guard len > 0 else {
return self
}
let relativeEnd = end - start
let relativeVector = CGPoint(x: relativeEnd.x / len, y: relativeEnd.y / len)
let sizedVector = CGPoint(x: relativeVector.x * toLength, y: relativeVector.y * toLength)
let absoluteVector = sizedVector + start
return CGLine(start: start, end: absoluteVector)
}
/// Flips a line vertically and horizontally from the start point.
func flipped() -> CGLine {
let relativeEnd = end - start
let flippedEnd = CGPoint(x: relativeEnd.x * -1, y: relativeEnd.y * -1)
return CGLine(start: start, end: flippedEnd + start)
}
/// Move the line to the new start point.
func transpose(_ toPoint: CGPoint) -> CGLine {
let diff = toPoint - start
let newEnd = end + diff
return CGLine(start: toPoint, end: newEnd)
}
}
infix operator +|
infix operator +-
extension CGPoint: Interpolatable {
/// Returns the distance between the receiver and the given point.
func distanceTo(_ a: CGPoint) -> CGFloat {
let xDist = a.x - x
let yDist = a.y - y
return CGFloat(sqrt((xDist * xDist) + (yDist * yDist)))
}
/// Returns the length between the receiver and *CGPoint.zero*
var vectorLength: CGFloat {
return distanceTo(.zero)
}
func rounded(decimal: CGFloat) -> CGPoint {
return CGPoint(x: (round(decimal * x) / decimal), y: (round(decimal * y) / decimal))
}
/**
Interpolates the receiver to the given Point by Amount.
- Parameter to: The Point to interpolate to.
- Parameter amount: The amount to interpolate from 0-1
```
let point = CGPoint(width: 50, height: 50)
let interpolated = rect.interpolateTo(CGPoint(width: 100, height: 100), amount: 0.5)
print(interpolated)
// Result: (x: 75, y: 75)
```
1. The amount can be greater than one and less than zero. The interpolation will not be clipped.
*/
func interpolate(_ to: CGPoint, amount: CGFloat) -> CGPoint {
return CGPoint(x: x.interpolateTo(to.x, amount: amount),
y: y.interpolateTo(to.y, amount: amount))
}
func interpolate(_ to: CGPoint, outTangent: CGPoint, inTangent: CGPoint, amount: CGFloat, maxIterations: Int = 3, samples: Int = 20, accuracy: CGFloat = 1) -> CGPoint {
if amount == 0 {
return self
}
if amount == 1 {
return to
}
if self.colinear(outTangent, inTangent) == true,
outTangent.colinear(inTangent, to) == true {
return interpolate(to, amount: amount)
}
let step = 1 / CGFloat(samples)
var points: [(point: CGPoint, distance: CGFloat)] = [(point: self, distance: 0)]
var totalLength: CGFloat = 0
var previousPoint = self
var previousAmount = CGFloat(0)
var closestPoint: Int = 0
while previousAmount < 1 {
previousAmount = previousAmount + step
if previousAmount < amount {
closestPoint = closestPoint + 1
}
let newPoint = self.pointOnPath(to, outTangent: outTangent, inTangent: inTangent, amount: previousAmount)
let distance = previousPoint.distanceTo(newPoint)
totalLength = totalLength + distance
points.append((point: newPoint, distance: totalLength))
previousPoint = newPoint
}
let accurateDistance = amount * totalLength
var point = points[closestPoint]
var foundPoint: Bool = false
var pointAmount: CGFloat = CGFloat(closestPoint) * step
var nextPointAmount: CGFloat = pointAmount + step
var refineIterations = 0
while foundPoint == false {
refineIterations = refineIterations + 1
/// First see if the next point is still less than the projected length.
let nextPoint = points[closestPoint + 1]
if nextPoint.distance < accurateDistance {
point = nextPoint
closestPoint = closestPoint + 1
pointAmount = CGFloat(closestPoint) * step
nextPointAmount = pointAmount + step
if closestPoint == points.count {
foundPoint = true
}
continue
}
if accurateDistance < point.distance {
closestPoint = closestPoint - 1
if closestPoint < 0 {
foundPoint = true
continue
}
point = points[closestPoint]
pointAmount = CGFloat(closestPoint) * step
nextPointAmount = pointAmount + step
continue
}
/// Now we are certain the point is the closest point under the distance
let pointDiff = nextPoint.distance - point.distance
let proposedPointAmount = ((accurateDistance - point.distance) / pointDiff).remap(fromLow: 0, fromHigh: 1, toLow: pointAmount, toHigh: nextPointAmount)
let newPoint = self.pointOnPath(to, outTangent: outTangent, inTangent: inTangent, amount: proposedPointAmount)
let newDistance = point.distance + point.point.distanceTo(newPoint)
pointAmount = proposedPointAmount
point = (point: newPoint, distance: newDistance)
if accurateDistance - newDistance <= accuracy ||
newDistance - accurateDistance <= accuracy {
foundPoint = true
}
if refineIterations == maxIterations {
foundPoint = true
}
}
return point.point
}
func pointOnPath(_ to: CGPoint, outTangent: CGPoint, inTangent: CGPoint, amount: CGFloat) -> CGPoint {
let a = self.interpolate(outTangent, amount: amount)
let b = outTangent.interpolate(inTangent, amount: amount)
let c = inTangent.interpolate(to, amount: amount)
let d = a.interpolate(b, amount: amount)
let e = b.interpolate(c, amount: amount)
let f = d.interpolate(e, amount: amount)
return f
}
func colinear(_ a: CGPoint, _ b: CGPoint) -> Bool {
let area = x * (a.y - b.y) + a.x * (b.y - y) + b.x * (y - a.y);
let accuracy: CGFloat = 0.05
if area < accuracy && area > -accuracy {
return true
}
return false
}
func interpolateTo(_ to: CGPoint, amount: CGFloat, spatialOutTangent: CGPoint?, spatialInTangent: CGPoint?) -> CGPoint {
guard let outTan = spatialOutTangent,
let inTan = spatialInTangent else {
return interpolate(to, amount: amount)
}
let cp1 = self + outTan
let cp2 = to + inTan
return interpolate(to, outTangent: cp1, inTangent: cp2, amount: amount)
}
/// Subtracts the given point from the receiving point.
func subtract(_ point: CGPoint) -> CGPoint {
return CGPoint(x: x - point.x,
y: y - point.y)
}
/// Adds the given point from the receiving point.
func add(_ point: CGPoint) -> CGPoint {
return CGPoint(x: x + point.x,
y: y + point.y)
}
var isZero: Bool {
return (x == 0 && y == 0)
}
/// Operator convenience to divide points with /
static func / (lhs: CGPoint, rhs: CGFloat) -> CGPoint {
return CGPoint(x: lhs.x / CGFloat(rhs), y: lhs.y / CGFloat(rhs))
}
/// Operator convenience to multiply points with *
static func * (lhs: CGPoint, rhs: CGFloat) -> CGPoint {
return CGPoint(x: lhs.x * CGFloat(rhs), y: lhs.y * CGFloat(rhs))
}
/// Operator convenience to add points with +
static func +(left: CGPoint, right: CGPoint) -> CGPoint {
return left.add(right)
}
/// Operator convenience to subtract points with -
static func -(left: CGPoint, right: CGPoint) -> CGPoint {
return left.subtract(right)
}
static func +|(left: CGPoint, right: CGFloat) -> CGPoint {
return CGPoint(x: left.x, y: left.y + right)
}
static func +-(left: CGPoint, right: CGFloat) -> CGPoint {
return CGPoint(x: left.x + right, y: left.y)
}
}