Cyborg/Arc.swift (149 lines of code) (raw):
//
// Copyright (c) 2018. Uber Technologies
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import CoreGraphics
struct EllipticArc {
let center: CGPoint
let radius: CGPoint
let xAngle: CGFloat
// These two equations come from http://www.spaceroots.org/documents/ellipse/elliptical-arc.pdf
func point(for pseudoAngle: CGFloat) -> CGPoint {
// 2.2.1 (3)
.init(
x: center.x + radius.x * cos(xAngle) * cos(pseudoAngle) - radius.y * sin(xAngle) * sin(pseudoAngle),
y: center.y + radius.x * sin(xAngle) * cos(pseudoAngle) + radius.y * cos(xAngle) * sin(pseudoAngle)
)
}
func derivative(for pseudoAngle: CGFloat) -> CGPoint {
// 2.2.1 (4)
.init(
x: -radius.x * cos(xAngle) * sin(pseudoAngle) - radius.y * sin(xAngle) * cos(pseudoAngle),
y: -radius.x * sin(xAngle) * sin(pseudoAngle) + radius.y * cos(xAngle) * cos(pseudoAngle)
)
}
}
func applyArc(to path: CGMutablePath,
in size: CGSize,
radius: CGPoint,
rotation: CGFloat,
largeArcFlag: CGFloat,
sweepFlag: CGFloat,
endPoint: CGPoint,
prior: PriorContext,
isRelative: Bool) -> PriorContext {
// see https://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes for explanation of how this works,
// and https://mortoray.com/2017/02/16/rendering-an-svg-elliptical-arc-as-bezier-curves/ for the adaptations
// that make it work properly.
let rotation = rotation * .pi / 180
let prior = prior.point
let endPoint = endPoint.times(size).add(isRelative ? prior : .zero)
var r = radius.times(size)
// eq 5.1
let transform = Matrix2x2(m00: cos(rotation),
m01: -sin(rotation),
m10: sin(rotation),
m11: cos(rotation))
let xy1 = transform.times(.init(x: (prior.x - endPoint.x) / 2,
y: (prior.y - endPoint.y) / 2))
// eq 5.2
// correction
let rxs = pow(r.x, 2)
let rys = pow(r.y, 2)
let x1ps = pow(xy1.x, 2)
let y1ps = pow(xy1.y, 2)
let cr = x1ps / rxs + y1ps / rys
if cr > 1 {
let s = sqrt(cr)
r.x *= s
r.y *= s
}
func paired(_ a: CGFloat, _ b: CGFloat) -> CGFloat {
return pow(a, 2) * pow(b, 2)
}
let dq = paired(r.x, xy1.y) + paired(r.y, xy1.x)
let c1 = CGPoint(x: (r.x * xy1.y) / r.y,
y: -(r.y * xy1.x) / r.x)
.times(
sqrt(
max(0, (paired(r.x, r.y) - dq) / dq)
)
)
.times(largeArcFlag != sweepFlag ? 1 : -1)
// eq 5.3
let transform2 = Matrix2x2(m00: cos(rotation),
m01: sin(rotation),
m10: -sin(rotation),
m11: cos(rotation))
let center = transform2
.times(c1)
.add(.init(x: (prior.x + endPoint.x) / 2,
y: (prior.y + endPoint.y) / 2))
// eq 5.4
let intermediateAngle = CGPoint(x: (xy1.x - c1.x) / r.x,
y: (xy1.y - c1.y) / r.y)
let startAngle = CGPoint(x: 1, y: 0).angle(with: intermediateAngle)
var delta = intermediateAngle.angle(with: .init(x: (-xy1.x - c1.x) / r.x,
y: (-xy1.y - c1.y) / r.y))
if delta > 0,
sweepFlag == 0 {
delta -= .pi * 2
} else if delta < 0,
sweepFlag == 1 {
delta += .pi * 2
}
let segments = Segments(start: startAngle,
delta: delta,
division: 6)
let arc = EllipticArc(center: center, radius: r, xAngle: rotation)
for (start, end) in segments {
let startPoint = arc.point(for: start)
// If our calculations disagree with the current point of the path, move to the point
// we computed, unless the difference is too small to matter. Entering this branch
// will likely lead to artifactsin when the VectorDrawable is displayed.
if !path.currentPoint.isWithinAPointOf(startPoint) {
path.move(to: startPoint)
}
let alpha: CGFloat = {
let denom = sqrt(
(4 + 3 * tan(pow((end - start) / 2, 2)) - 1)
) - 1
return sin(end - start) * denom / 3
}()
let endPoint = arc.point(for: end)
let c1 = arc.derivative(for: start).times(alpha).add(startPoint)
let c2 = endPoint.subtract(arc.derivative(for: end).times(alpha))
path
.addCurve(to: endPoint,
control1: c1,
control2: c2)
}
return endPoint.asPriorContext
}
fileprivate struct Segments: Sequence {
typealias Element = (CGFloat, CGFloat)
let start: CGFloat
let delta: CGFloat
let division: Int
init(start: CGFloat, delta: CGFloat, division: Int) {
self.start = start
self.delta = delta
self.division = division
}
func makeIterator() -> Segments.Iterator {
Iterator(current: start,
currentIndex: 0,
delta: delta,
division: division)
}
struct Iterator: IteratorProtocol {
typealias Element = (CGFloat, CGFloat)
var current: CGFloat
var currentIndex: Int = 0
let delta: CGFloat
let division: Int
mutating func next() -> (CGFloat, CGFloat)? {
let last = current
if currentIndex < division {
currentIndex += 1
current += delta / CGFloat(division)
return (last, current)
} else {
return nil
}
}
}
}
struct Matrix2x2 {
let m00: CGFloat
let m01: CGFloat
let m10: CGFloat
let m11: CGFloat
func times(_ vector: CGPoint) -> CGPoint {
let x = m00 * vector.x + m10 * vector.y
let y = m01 * vector.x + m11 * vector.y
return CGPoint(x: x, y: y)
}
}