Handle quad curves

This commit is contained in:
Rob Napier
2019-12-08 13:53:37 -05:00
parent e7bc757df8
commit 74c81a2ca8
2 changed files with 121 additions and 88 deletions

View File

@@ -82,11 +82,7 @@ public class PathTextView: UIView {
public override func draw(_ rect: CGRect) {
let tangents = path.tangents(atLocations: locations.map { $0.x })
let sections = path.sections()
guard let pathStart = sections.first?.start else { return }
let tangents = path.getTangents(atLocations: locations.map { $0.x })
let context = UIGraphicsGetCurrentContext()!
@@ -112,23 +108,6 @@ public class PathTextView: UIView {
}
}
// The Bezier function at t
func bezier(_ t: CGFloat, _ P0: CGFloat, _ P1: CGFloat, _ P2: CGFloat, _ P3: CGFloat) -> CGFloat {
(1-t)*(1-t)*(1-t) * P0
+ 3 * (1-t)*(1-t) * t * P1
+ 3 * (1-t) * t*t * P2
+ t*t*t * P3
}
// The slope of the Bezier function at t
func bezierPrime(_ t: CGFloat, _ P0: CGFloat, _ P1: CGFloat, _ P2: CGFloat, _ P3: CGFloat) -> CGFloat {
0
- 3 * (1-t)*(1-t) * P0
+ (3 * (1-t)*(1-t) * P1) - (6 * t * (1-t) * P1)
- (3 * t*t * P2) + (6 * t * (1-t) * P2)
+ 3 * t*t * P3
}
extension CGPoint {
func distance(to other: CGPoint) -> CGFloat {
let dx = x - other.x
@@ -194,7 +173,6 @@ struct PathText_Previews: PreviewProvider {
}
}
static func LineAndCurveView() -> some View {
let P0 = CGPoint(x: 50, y: 500)
let P1 = CGPoint(x: 150, y: 300)
@@ -214,12 +192,28 @@ struct PathText_Previews: PreviewProvider {
path.stroke(Color.blue, lineWidth: 2)
}
}
static func QuadCurveView() -> some View {
let P0 = CGPoint(x: 50, y: 500)
let P1 = CGPoint(x: 300, y: 300)
let P2 = CGPoint(x: 650, y: 500)
let path = Path() {
$0.move(to: P0)
$0.addQuadCurve(to: P2, control: P1)
}
return PathText(text: text, path: path)
}
static var previews: some View {
Group {
CurveView()
LineView()
LinesView()
LineAndCurveView()
QuadCurveView()
}
}
}
@@ -241,7 +235,6 @@ extension PathSection {
// of guesses, but this is tricky since if we go too far out, the
// curve might loop back on leading to incorrect results. Tuning
// kStep is good start.
// func getOffset(atDistance distance: CGFloat, from point: CGPoint, offset: CGFloat) -> CGFloat {
let point = lastTangent.point
let step: CGFloat = 0.001 // 0.0001 - 0.001 work well
@@ -286,7 +279,6 @@ extension Path {
start = nil
case .move(to: let p):
// sections.append(PathMoveSection(to: p))
start = start ?? p
current = p
@@ -301,17 +293,16 @@ extension Path {
current = p
case let .quadCurve(to: p2, control: p1):
fatalError()
// sections.append(PathQuadCurveSection(p0: current ?? .zero, p1: p1, p2: p2))
// start = start ?? .zero
// current = p2
sections.append(PathQuadCurveSection(p0: current ?? .zero, p1: p1, p2: p2))
start = start ?? .zero
current = p2
}
}
return sections
}
// Locations must be in ascending order
func tangents(atLocations locations: [CGFloat]) -> [PathTangent] {
func getTangents(atLocations locations: [CGFloat]) -> [PathTangent] {
assert(locations == locations.sorted())
var tangents: [PathTangent] = []
@@ -341,9 +332,9 @@ extension Path {
lastLocation = location
locations = locations.dropFirst()
case .insufficient(remainingLinearDistance: _):
case .insufficient(remainingLinearDistance: let remaining):
lastTangent = nil
lastLocation = location
lastLocation = location + remaining
sections = sections.dropFirst()
}
}
@@ -368,11 +359,36 @@ struct PathLineSection: PathSection {
}
}
//struct PathQuadCurveSection: PathSection {
// let p0, p1, p2: CGPoint
// var start: CGPoint { p0 }
// var end: CGPoint { p2 }
//}
struct PathQuadCurveSection: PathSection {
let p0, p1, p2: CGPoint
var start: CGPoint { p0 }
var end: CGPoint { p2 }
func getTangent(t: CGFloat) -> PathTangent {
let dx = bezierPrime(t, p0.x, p1.x, p2.x)
let dy = bezierPrime(t, p0.y, p1.y, p2.y)
let x = bezier(t, p0.x, p1.x, p2.x)
let y = bezier(t, p0.y, p1.y, p2.y)
return PathTangent(t: t,
point: CGPoint(x: x, y: y),
angle: atan2(dy, dx))
}
// The quadratic Bezier function at t
private func bezier(_ t: CGFloat, _ P0: CGFloat, _ P1: CGFloat, _ P2: CGFloat) -> CGFloat {
(1-t)*(1-t) * P0
+ 2 * (1-t) * t * P1
+ t*t * P2
}
// The slope of the quadratic Bezier function at t
private func bezierPrime(_ t: CGFloat, _ P0: CGFloat, _ P1: CGFloat, _ P2: CGFloat) -> CGFloat {
2 * (1-t) * (P1 - P0)
+ 2 * t * (P2 - P1)
}
}
struct PathCurveSection: PathSection {
@@ -391,4 +407,21 @@ struct PathCurveSection: PathSection {
point: CGPoint(x: x, y: y),
angle: atan2(dy, dx))
}
// The cubic Bezier function at t
private func bezier(_ t: CGFloat, _ P0: CGFloat, _ P1: CGFloat, _ P2: CGFloat, _ P3: CGFloat) -> CGFloat {
(1-t)*(1-t)*(1-t) * P0
+ 3 * (1-t)*(1-t) * t * P1
+ 3 * (1-t) * t*t * P2
+ t*t*t * P3
}
// The slope of the cubic Bezier function at t
private func bezierPrime(_ t: CGFloat, _ P0: CGFloat, _ P1: CGFloat, _ P2: CGFloat, _ P3: CGFloat) -> CGFloat {
0
- 3 * (1-t)*(1-t) * P0
+ (3 * (1-t)*(1-t) * P1) - (6 * t * (1-t) * P1)
- (3 * t*t * P2) + (6 * t * (1-t) * P2)
+ 3 * t*t * P3
}
}

View File

@@ -42,7 +42,7 @@ final class PathTextTests: XCTestCase {
XCTAssertEqual(section.p2, P2)
XCTAssertEqual(section.p3, P3)
let tangents = path.tangents(atLocations: [0, 100, 200, 300, 400, 500, 600])
let tangents = path.getTangents(atLocations: [0, 100, 200, 300, 400, 500, 600])
XCTAssertEqual(tangents, [
PathTangent(t: 0,
point: P0,
@@ -90,7 +90,7 @@ final class PathTextTests: XCTestCase {
XCTAssertEqual(section.start, P0)
XCTAssertEqual(section.end, P1)
let tangents = path.tangents(atLocations: [0, 100, 200, 300, 400, 500, 600, 700])
let tangents = path.getTangents(atLocations: [0, 100, 200, 300, 400, 500, 600, 700])
XCTAssertEqual(tangents, [
PathTangent(t: 0, point: CGPoint(x: 0, y: 0), angle: 0),
@@ -126,7 +126,7 @@ final class PathTextTests: XCTestCase {
XCTAssertEqual(section.start, P0)
XCTAssertEqual(section.end, P1)
let tangents = path.tangents(atLocations: [0, 100, 200, 300, 400, 500, 600, 700])
let tangents = path.getTangents(atLocations: [0, 100, 200, 300, 400, 500, 600, 700])
let angle: CGFloat = atan(1)
@@ -142,54 +142,54 @@ final class PathTextTests: XCTestCase {
])
}
func testTwoLines() {
let P0 = CGPoint(x: 0, y: 0)
let P1 = CGPoint(x: 400, y: 400)
let P2 = CGPoint(x: 800, y: 0)
let path = Path() {
$0.move(to: P0)
$0.addLine(to: P1)
$0.addLine(to: P2)
}
let sections = path.sections()
XCTAssertEqual(sections.count, 2)
guard let section1 = sections.first as? PathLineSection else {
XCTFail()
return
}
XCTAssertEqual(section1.start, P0)
XCTAssertEqual(section1.end, P1)
guard let section2 = sections.dropFirst().first as? PathLineSection else {
XCTFail()
return
}
XCTAssertEqual(section2.start, P1)
XCTAssertEqual(section2.end, P2)
let tangents = path.tangents(atLocations: [0, 100, 200, 300, 400, 500, 600, 700])
let angle: CGFloat = atan(1)
XCTAssertEqual(tangents, [
PathTangent(t: 0.0, point: CGPoint(x: 0.0, y: 0.0), angle: angle),
PathTangent(t: 0.08900000000000007, point: CGPoint(x: 71.200000000000050, y: 71.200000000000050), angle: angle),
PathTangent(t: 0.17800000000000013, point: CGPoint(x: 142.40000000000010, y: 142.40000000000010), angle: angle),
PathTangent(t: 0.26700000000000020, point: CGPoint(x: 213.60000000000014, y: 213.60000000000014), angle: angle),
PathTangent(t: 0.35600000000000026, point: CGPoint(x: 284.80000000000020, y: 284.80000000000020), angle: angle),
PathTangent(t: 0.44500000000000034, point: CGPoint(x: 356.00000000000030, y: 356.00000000000030), angle: angle),
PathTangent(t: 0.53400000000000040, point: CGPoint(x: 427.20000000000030, y: 427.20000000000030), angle: angle),
PathTangent(t: 0.62300000000000040, point: CGPoint(x: 498.40000000000040, y: 498.40000000000040), angle: angle),
])
}
// func testTwoLines() {
//
// let P0 = CGPoint(x: 0, y: 0)
// let P1 = CGPoint(x: 400, y: 400)
// let P2 = CGPoint(x: 800, y: 0)
//
// let path = Path() {
// $0.move(to: P0)
// $0.addLine(to: P1)
// $0.addLine(to: P2)
// }
//
// let sections = path.sections()
//
// XCTAssertEqual(sections.count, 2)
//
// guard let section1 = sections.first as? PathLineSection else {
// XCTFail()
// return
// }
//
// XCTAssertEqual(section1.start, P0)
// XCTAssertEqual(section1.end, P1)
//
// guard let section2 = sections.dropFirst().first as? PathLineSection else {
// XCTFail()
// return
// }
//
// XCTAssertEqual(section2.start, P1)
// XCTAssertEqual(section2.end, P2)
//
//
// let tangents = path.getTangents(atLocations: [0, 100, 200, 300, 400, 500, 600, 700])
//
// let angle: CGFloat = atan(1)
//
// XCTAssertEqual(tangents, [
// PathTangent(t: 0.0, point: CGPoint(x: 0.0, y: 0.0), angle: angle),
// PathTangent(t: 0.08900000000000007, point: CGPoint(x: 71.200000000000050, y: 71.200000000000050), angle: angle),
// PathTangent(t: 0.17800000000000013, point: CGPoint(x: 142.40000000000010, y: 142.40000000000010), angle: angle),
// PathTangent(t: 0.26700000000000020, point: CGPoint(x: 213.60000000000014, y: 213.60000000000014), angle: angle),
// PathTangent(t: 0.35600000000000026, point: CGPoint(x: 284.80000000000020, y: 284.80000000000020), angle: angle),
// PathTangent(t: 0.44500000000000034, point: CGPoint(x: 356.00000000000030, y: 356.00000000000030), angle: angle),
// PathTangent(t: 0.53400000000000040, point: CGPoint(x: 427.20000000000030, y: 427.20000000000030), angle: angle),
// PathTangent(t: 0.62300000000000040, point: CGPoint(x: 498.40000000000040, y: 498.40000000000040), angle: angle),
// ])
// }
static var allTests = [