Last active
January 5, 2017 21:55
-
-
Save im-fredrik/3ccf4df0e683402dd0516aeaf33bcb86 to your computer and use it in GitHub Desktop.
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
| //: 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