mirror of
https://github.com/zhigang1992/CurvyText.git
synced 2026-03-26 10:14:20 +08:00
Handle quad curves
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user