Created
June 8, 2015 14:29
-
-
Save danscan/c4bc627429e6e414c269 to your computer and use it in GitHub Desktop.
PointSequence.swift
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
// | |
// PointSequence.swift | |
// ThoughtPad | |
// | |
// Created by Daniel Scanlon on 6/5/15. | |
// Copyright (c) 2015 Present. All rights reserved. | |
// | |
import UIKit | |
class PointSequence: Printable { | |
// MARK: Printable Conformance | |
var description: String { | |
return points.description | |
} | |
// MARK: State | |
var points: [CGPoint] | |
var closeDistanceThresholdFactor: CGFloat = 0.1 | |
// MARK: Dynamic Properties | |
var firstPoint: CGPoint { | |
return points.first ?? CGPointMake(0, 0) | |
} | |
var lastPoint: CGPoint { | |
return points.last ?? CGPointMake(0, 0) | |
} | |
var bezierPath: UIBezierPath { | |
// Get Hermite-interpolated bezier path | |
let bezierPath = self.getHermiteInterpolatedBezierPath() | |
return bezierPath | |
} | |
var closed: Bool { | |
// Get bounds | |
let bounds = self.getBounds() | |
// Get close distance (x,y) thresholds via factor * bounds dimension | |
let closeDistanceXThreshold = bounds.width * closeDistanceThresholdFactor | |
let closeDistanceYThreshold = bounds.height * closeDistanceThresholdFactor | |
// Get close distances | |
let closeDistanceX = abs(lastPoint.x - firstPoint.x) | |
let closeDistanceY = abs(lastPoint.y - firstPoint.y) | |
// Closed if (x,y) close distances are within close distance thresholds | |
if closeDistanceX < closeDistanceXThreshold && closeDistanceY < closeDistanceYThreshold { | |
return true | |
} else { | |
return false | |
} | |
} | |
// MARK: Init | |
init(initialPoint: CGPoint) { | |
self.points = [initialPoint] | |
} | |
init(points: [CGPoint]) { | |
self.points = points | |
} | |
// MARK: Public mutation methods | |
func appendPoint(point: CGPoint) { | |
points.append(point) | |
} | |
// MARK: Private helper methods | |
private func getBezierPath() -> UIBezierPath { | |
// Create bezier path | |
let bezierPath = UIBezierPath() | |
// Move bezier path to first point | |
bezierPath.moveToPoint(firstPoint) | |
// Draw lines to subsequent points in sequence | |
for point: CGPoint in points { | |
bezierPath.addLineToPoint(point) | |
} | |
return bezierPath | |
} | |
private func getBounds() -> CGRect { | |
// Get bounds of bezier path | |
let bezierPath = self.getBezierPath() | |
let bounds = bezierPath.bounds | |
return bounds | |
} | |
private func getHermiteInterpolatedBezierPath() -> UIBezierPath { | |
// Create bezier path, and move to first point | |
let bezierPath = UIBezierPath() | |
bezierPath.moveToPoint(firstPoint) | |
// Return simple bezier path if < 2 points | |
if points.count < 2 { | |
return self.getBezierPath() | |
} | |
// Get number of curves in path | |
var numberOfCurves = closed ? points.count : points.count - 1 | |
// Iterate through points in curves | |
for var i=0; i < numberOfCurves; ++i { | |
var nextPointIndex = (i + 1) % points.count | |
var previousPointIndex = (i - 1) < 0 ? points.count - 1 : i - 1 | |
var currentPoint: CGPoint = points[i] | |
var previousPoint: CGPoint = points[previousPointIndex] | |
var nextPoint: CGPoint = points[nextPointIndex] | |
var endPoint: CGPoint = nextPoint | |
// Calculate middle between current, previous, and next points | |
var middleX: CGFloat | |
var middleY: CGFloat | |
if closed || i > 0 { | |
middleX = (nextPoint.x - currentPoint.x)*0.5 + (currentPoint.x - previousPoint.x)*0.5 | |
middleY = (nextPoint.y - currentPoint.y)*0.5 + (currentPoint.y - previousPoint.y)*0.5 | |
} else { | |
middleX = (nextPoint.x - currentPoint.x)*0.5 | |
middleY = (nextPoint.y - currentPoint.y)*0.5 | |
} | |
// Create control point 1 | |
let controlPoint1X: CGFloat = currentPoint.x + middleX / 3 | |
let controlPoint1Y: CGFloat = currentPoint.y + middleY / 3 | |
let controlPoint1 = CGPointMake(controlPoint1X, controlPoint1Y) | |
// Advance current point to next point | |
currentPoint = points[nextPointIndex] | |
nextPointIndex = (nextPointIndex + 1) % points.count | |
previousPointIndex = i | |
previousPoint = points[previousPointIndex] | |
nextPoint = points[nextPointIndex] | |
// Calculate middle between current, previous, and next points | |
if closed || i < numberOfCurves - 1 { | |
middleX = (nextPoint.x - currentPoint.x)*0.5 + (currentPoint.x - previousPoint.x)*0.5 | |
middleY = (nextPoint.y - currentPoint.y)*0.5 + (currentPoint.y - previousPoint.y)*0.5 | |
} else { | |
middleX = (nextPoint.x - currentPoint.x)*0.5 | |
middleY = (nextPoint.y - currentPoint.y)*0.5 | |
} | |
// Create control point 2 | |
let controlPoint2X: CGFloat = currentPoint.x + middleX / 3 | |
let controlPoint2Y: CGFloat = currentPoint.y + middleY / 3 | |
let controlPoint2 = CGPointMake(controlPoint1X, controlPoint1Y) | |
// Add curve to to end point through control points to bezier path | |
bezierPath.addCurveToPoint(endPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2) | |
} | |
// Close bezier path if this is closed | |
if closed { | |
bezierPath.closePath() | |
} | |
return bezierPath | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment