Skip to content

Instantly share code, notes, and snippets.

@im-fredrik
Last active January 5, 2017 21:55
Show Gist options
  • Select an option

  • Save im-fredrik/3ccf4df0e683402dd0516aeaf33bcb86 to your computer and use it in GitHub Desktop.

Select an option

Save im-fredrik/3ccf4df0e683402dd0516aeaf33bcb86 to your computer and use it in GitHub Desktop.
//: Playground - noun: a place where people can play
import UIKit
import Foundation
import CoreGraphics
private func clamp(v: CGFloat, _ from: CGFloat, _ to: CGFloat) -> CGFloat {
return (v - from) / (to - from)
}
func map(x:CGFloat, _ in_min:CGFloat, _ in_max:CGFloat, _ out_min:CGFloat, _ out_max:CGFloat) -> CGFloat{
return (((x) - (in_min)) * ((out_max) - (out_min)) / ((in_max) - (in_min)) + (out_min))
}
func bezierEasing(c0:CGFloat, _ c1:CGFloat, _ c2:CGFloat, _ c3:CGFloat) -> (CGFloat) -> CGFloat {
func getSplineValue(aX: CGFloat, mX1: CGFloat, mY1: CGFloat, mX2: CGFloat, mY2: CGFloat) -> CGFloat {
if(mX1 == mY1 && mX2 == mY2) {
return aX
}
func A(aA1: CGFloat, _ aA2: CGFloat) -> CGFloat {
return 1.0 - 3.0 * aA2 + 3.0 * aA1
}
func B(aA1: CGFloat, _ aA2: CGFloat) -> CGFloat {
return 3.0 * aA2 - 6.0 * aA1
}
func C(aA1: CGFloat) -> CGFloat {
return 3.0 * aA1
}
func calcBezier(aT: CGFloat, aA1: CGFloat, aA2: CGFloat) -> CGFloat {
return ((A(aA1, aA2)*aT + B(aA1, aA2))*aT + C(aA1))*aT
}
func getSlope(aT: CGFloat, aA1: CGFloat, aA2: CGFloat) -> CGFloat {
return 3.0 * A(aA1, aA2)*aT*aT + 2.0 * B(aA1, aA2) * aT + C(aA1)
}
func newtonRaphsonIterate(aX: CGFloat) -> CGFloat {
var aGuessT = aX;
for (var i = 0; i < 3; ++i) {
let currentX = calcBezier(aGuessT, aA1: mX1, aA2: mX2) - aX
let currentSlope = getSlope(aGuessT, aA1: mX1, aA2:mX2)
if (currentSlope == 0.0) {
return aGuessT
}
aGuessT -= currentX / currentSlope
}
return aGuessT;
}
func getTForX(aX: CGFloat) -> CGFloat {
return newtonRaphsonIterate(aX)
}
return calcBezier(getTForX(aX), aA1: mY1, aA2: mY2)
}
func result(value: CGFloat) -> CGFloat {
return getSplineValue(value, mX1: c0, mY1: c1, mX2: c2, mY2: c3)
}
return result
}
private enum Coordinates {
case Absolute
case Relative
}
private let numberSet = NSCharacterSet(charactersInString: "-.0123456789eE")
private let numberFormatter = NSNumberFormatter()
private extension SVGPath {
class func parseNumbers (numbers: String) -> [CGFloat] {
var all:[String] = []
var curr = ""
var last = ""
for char in numbers.unicodeScalars {
let next = String(char)
if next == "-" && last != "" && last != "E" && last != "e" {
if curr.utf16.count > 0 {
all.append(curr)
}
curr = next
} else if numberSet.longCharacterIsMember(char.value) {
curr += next
} else if curr.utf16.count > 0 {
all.append(curr)
curr = ""
}
last = next
}
all.append(curr)
return all.map() {
(number: String) -> CGFloat in
if let num = numberFormatter.numberFromString(number)?.doubleValue {
return CGFloat(num)
}
return 0.0
}
}
}
// MARK: Commands
private struct SVGCommand {
public var point:CGPoint
public var control1:CGPoint
public var control2:CGPoint
public var type:Kind
public enum Kind {
case Move
case Line
case CubeCurve
case QuadCurve
case Close
}
public init () {
let point = CGPoint()
self.init(point, point, point, type: .Close)
}
public init (_ x: CGFloat, _ y: CGFloat, type: Kind) {
let point = CGPoint(x: x, y: y)
self.init(point, point, point, type: type)
}
public init (_ cx: CGFloat, _ cy: CGFloat, _ x: CGFloat, _ y: CGFloat) {
let control = CGPoint(x: cx, y: cy)
self.init(control, control, CGPoint(x: x, y: y), type: .QuadCurve)
}
public init (_ cx1: CGFloat, _ cy1: CGFloat, _ cx2: CGFloat, _ cy2: CGFloat, _ x: CGFloat, _ y: CGFloat) {
self.init(CGPoint(x: cx1, y: cy1), CGPoint(x: cx2, y: cy2), CGPoint(x: x, y: y), type: .CubeCurve)
}
public init (_ control1: CGPoint, _ control2: CGPoint, _ point: CGPoint, type: Kind) {
self.point = point
self.control1 = control1
self.control2 = control2
self.type = type
}
private func relativeTo (other:SVGCommand?) -> SVGCommand {
if let otherPoint = other?.point {
return SVGCommand(control1 + otherPoint, control2 + otherPoint, point + otherPoint, type: type)
}
return self
}
}
private func +(a:CGPoint, b:CGPoint) -> CGPoint {
return CGPoint(x: a.x + b.x, y: a.y + b.y)
}
private func -(a:CGPoint, b:CGPoint) -> CGPoint {
return CGPoint(x: a.x - b.x, y: a.y - b.y)
}
private typealias SVGCommandBuilder = ([CGFloat], SVGCommand?, Coordinates) -> SVGCommand
private func take(numbers: [CGFloat], stride: Int, coords: Coordinates, last: SVGCommand?, callback: SVGCommandBuilder) -> [SVGCommand] {
var out: [SVGCommand] = []
var lastCommand:SVGCommand? = last
let count = (numbers.count / stride) * stride
var nums:[CGFloat] = [0, 0, 0, 0, 0, 0];
for var i = 0; i < count; i += stride {
for j in 0 ..< stride {
nums[j] = numbers[i + j]
}
lastCommand = callback(nums, lastCommand, coords)
out.append(lastCommand!)
}
return out
}
private func moveTo(numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand {
return SVGCommand(numbers[0], numbers[1], type: .Move)
}
private func lineTo(numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand {
return SVGCommand(numbers[0], numbers[1], type: .Line)
}
private func lineToVertical(numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand {
return SVGCommand(coords == .Absolute ? last?.point.x ?? 0 : 0, numbers[0], type: .Line)
}
private func lineToHorizontal(numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand {
return SVGCommand(numbers[0], coords == .Absolute ? last?.point.y ?? 0 : 0, type: .Line)
}
private func quadBroken(numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand {
return SVGCommand(numbers[0], numbers[1], numbers[2], numbers[3])
}
private func quadSmooth(numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand {
var lastControl = last?.control1 ?? CGPoint()
let lastPoint = last?.point ?? CGPoint()
if (last?.type ?? .Line) != .QuadCurve {
lastControl = lastPoint
}
var control = lastPoint - lastControl
if coords == .Absolute {
control = control + lastPoint
}
return SVGCommand(control.x, control.y, numbers[0], numbers[1])
}
private func cubeBroken(numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand {
return SVGCommand(numbers[0], numbers[1], numbers[2], numbers[3], numbers[4], numbers[5])
}
private func cubeSmooth(numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand {
var lastControl = last?.control2 ?? CGPoint()
let lastPoint = last?.point ?? CGPoint()
if (last?.type ?? .Line) != .CubeCurve {
lastControl = lastPoint
}
var control = lastPoint - lastControl
if coords == .Absolute {
control = control + lastPoint
}
return SVGCommand(control.x, control.y, numbers[0], numbers[1], numbers[2], numbers[3])
}
private func close(numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand {
return SVGCommand()
}
private class SVGPath {
private var commands: [SVGCommand] = []
private var builder: SVGCommandBuilder = moveTo
private var coords: Coordinates = .Absolute
private var stride: Int = 2
private var numbers = ""
init (_ string: String) {
for char in string.characters {
switch char {
case "M": use(.Absolute, 2, moveTo)
case "m": use(.Relative, 2, moveTo)
case "L": use(.Absolute, 2, lineTo)
case "l": use(.Relative, 2, lineTo)
case "V": use(.Absolute, 1, lineToVertical)
case "v": use(.Relative, 1, lineToVertical)
case "H": use(.Absolute, 1, lineToHorizontal)
case "h": use(.Relative, 1, lineToHorizontal)
case "Q": use(.Absolute, 4, quadBroken)
case "q": use(.Relative, 4, quadBroken)
case "T": use(.Absolute, 2, quadSmooth)
case "t": use(.Relative, 2, quadSmooth)
case "C": use(.Absolute, 6, cubeBroken)
case "c": use(.Relative, 6, cubeBroken)
case "S": use(.Absolute, 4, cubeSmooth)
case "s": use(.Relative, 4, cubeSmooth)
case "Z": use(.Absolute, 1, close)
case "z": use(.Absolute, 1, close)
default: numbers.append(char)
}
}
finishLastCommand()
}
private func use (coords: Coordinates, _ stride: Int, _ builder: SVGCommandBuilder) {
finishLastCommand()
self.builder = builder
self.coords = coords
self.stride = stride
}
private func finishLastCommand () {
for command in take(SVGPath.parseNumbers(numbers), stride: stride, coords: coords, last: commands.last, callback: builder) {
commands.append(coords == .Relative ? command.relativeTo(commands.last) : command)
}
numbers = ""
}
}
private func applyCommands (svgPath: String, path: UIBezierPath) {
for command in SVGPath(svgPath).commands {
switch command.type {
case .Move: path.moveToPoint(command.point)
case .Line: path.addLineToPoint(command.point)
case .QuadCurve: path.addQuadCurveToPoint(command.point, controlPoint: command.control1)
case .CubeCurve: path.addCurveToPoint(command.point, controlPoint1: command.control1, controlPoint2: command.control2)
case .Close: path.closePath()
}
}
}
struct Util {
func clamp(v: CGFloat, _ from: CGFloat, _ to: CGFloat) -> CGFloat {
return (from - v) / (to - from)
}
}
private class BezierSegment {
var yDiff: CGFloat
var xDiff: CGFloat
var from: CGPoint
var to: CGPoint
var fromControlPoint: CGPoint
var toControlPoint: CGPoint
var rawEasing: (CGFloat) -> CGFloat
init(from: CGPoint, fromControlPoint: CGPoint, to: CGPoint, toControlPoint: CGPoint) {
self.from = from
self.to = to
self.fromControlPoint = fromControlPoint
self.toControlPoint = toControlPoint
self.yDiff = to.y - from.y
self.xDiff = to.x - from.x
self.rawEasing = bezierEasing(
clamp(fromControlPoint.x, from.x, to.x),
clamp(fromControlPoint.y, from.y, to.y),
clamp(toControlPoint.x, from.x, to.x),
clamp(toControlPoint.y, from.y, to.y))
}
func easing(v: CGFloat) -> CGFloat {
let relativeV = (v - from.x) / (to.x - from.x)
let easingValue = rawEasing(relativeV)
let relativeY = from.y + (easingValue) * (to.y - from.y)
return relativeY
}
}
class SvgToEasingHelper {
func easingFromSvgPath(svgPath: String) -> (CGFloat) -> CGFloat {
var from = CGPoint.zero
let commands = SVGPath(svgPath).commands
var bezierFunctions = [BezierSegment]()
let start: CGPoint = commands.first!.point
let last: CGPoint = commands.last!.point
var minY: CGFloat = CGFloat.max
var maxY: CGFloat = CGFloat.min
for command in commands {
maxY = max(maxY, command.point.y)
minY = min(minY, command.point.y)
switch command.type {
case .Line:
bezierFunctions.append(
BezierSegment(from: from, fromControlPoint: from, to: command.point, toControlPoint: command.point))
break
case .CubeCurve:
bezierFunctions.append(
BezierSegment(from: from, fromControlPoint: command.control1, to: command.point, toControlPoint: command.control2))
break
//case .Close: break
default:
break
}
from = command.point
}
return { v in
var i: Int = 0
let totLength = last.x - start.x
let rawV = start.x + v * totLength
while(i < bezierFunctions.count) {
if(rawV < bezierFunctions[i].to.x) {
let temp = bezierFunctions[i].easing(rawV)
return (temp - minY) / (maxY - minY)
}
i += 1
}
return 1
}
}
func generatePoints(curve: ((CGFloat) -> CGFloat), points: Int) -> String {
var str = "var points: [CGPoint] = ["
for n in 0...(points - 1) {
let x = CGFloat(n) / CGFloat(points)
str += "CGPoint(x: \(x), y: \(curve(x)))"
if(n != (points - 1)) {
str += ", "
}
}
str += "]"
return str
}
func writeFile(text: String, file: String) {
if let dir : NSString = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.AllDomainsMask, true).first {
let path = dir.stringByAppendingPathComponent(file)
//writing
do {
try text.writeToFile(path, atomically: false, encoding: NSUTF8StringEncoding)
}
catch {/* error handling here */
//reading
}
}
}
static func exportSvgPath(path: String, file: String, points: Int) {
let helper = SvgToEasingHelper()
let easingFunction = helper.easingFromSvgPath(path)
let pointsString = helper.generatePoints(easingFunction, points: points)
helper.writeFile(pointsString, file: file)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment