in Cyborg/Arc.swift [44:140]
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
}