Skip to content

Instantly share code, notes, and snippets.

@chris-horner
Created October 27, 2019 09:59
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chris-horner/80837fd3f3ac54052b766648d80ddbd2 to your computer and use it in GitHub Desktop.
Save chris-horner/80837fd3f3ac54052b766648d80ddbd2 to your computer and use it in GitHub Desktop.
A simplified implementation of the library found at https://github.com/neild001/ArcMotionPlus
import android.graphics.Path
import android.graphics.PointF
import android.transition.PathMotion
import kotlin.math.atan
import kotlin.math.sin
import kotlin.math.sqrt
import kotlin.math.tan
/**
* A PathMotion that generates a curved path along an arc on an imaginary circle containing the two points. The two
* points reside on the circle and a line between them describes a chord for the circle that is symmetrical.
*
* A perpendicular line from the midpoint of the line between the two points will intersect the centre of the circle.
* Everything about the line from the centre of the circle to the midpoint of the chord is symmetrical.
*
* Adapted from Neil Davies' original work: https://github.com/neild001/ArcMotionPlus
*/
class ArcMotionPlus(private val arcAngle: Float = 90f, private val reflected: Boolean = false) : PathMotion() {
override fun getPath(startX: Float, startY: Float, endX: Float, endY: Float): Path {
val start = PointF(startX, startY)
val end = PointF(endX, endY)
require(!(start.x == end.x && start.y == end.y)) { "Start and end points cannot be the same." }
require(arcAngle in 1.0..179.0) { "Arc angle must be between 1 and 179 degrees." }
val angleRadians: Double = Math.toRadians(arcAngle.toDouble())
val deltaX: Float = start.x - end.x
val deltaY: Float = start.y - end.y
val halfChordLength: Float = sqrt((deltaX * deltaX + deltaY * deltaY).toDouble()).toFloat() / 2f
val radius: Float = halfChordLength / sin(angleRadians / 2f).toFloat()
// The length of the line from the start or end point to the control point.
val controlLength: Float = (4f / 3f * tan(angleRadians / 4)).toFloat() * radius
val angleToControl: Float = Math.toDegrees(atan((controlLength / radius).toDouble())).toFloat()
// The mid point of the line from start to end.
val midPointChord = PointF((start.x + end.x) / 2, (start.y + end.y) / 2f)
// Angle between line from circle centre to control point, and line between control points.
val chordRadiusAngle: Float = 180f - 90f - arcAngle / 2f
fun getTrianglePoint(angle: Float, a: PointF, b: PointF): PointF {
val angleInRadians = Math.toRadians(angle.toDouble())
val thirdPoint = PointF()
thirdPoint.x = tan(angleInRadians).toFloat() * (b.y - a.y) * -1f
thirdPoint.y = tan(angleInRadians).toFloat() * (b.x - a.x)
thirdPoint.x = thirdPoint.x + a.x
thirdPoint.y = thirdPoint.y + a.y
return thirdPoint
}
fun getReflectedPointAboutLine(start: PointF, end: PointF, reflect: PointF): PointF {
val reflectedPoint = PointF()
if (start.x != end.x) {
val m = (start.y - end.y) / (start.x - end.x)
val c = end.y - m * end.x
val d = (reflect.x + (reflect.y - c) * m) / (1 + m * m)
reflectedPoint.x = 2 * d - reflect.x
reflectedPoint.y = 2f * d * m - reflect.y + 2 * c
} else {
reflectedPoint.y = reflect.y
reflectedPoint.x = start.x - (reflect.x - start.x)
}
return reflectedPoint
}
val centre: PointF = getTrianglePoint(chordRadiusAngle, midPointChord, end)
val controlPoint2: PointF = getTrianglePoint(angleToControl, end, centre)
val controlPoint1: PointF = getReflectedPointAboutLine(centre, midPointChord, controlPoint2)
val reflectedControlPoint1: PointF = getReflectedPointAboutLine(start, end, controlPoint1)
val reflectedControlPoint2: PointF = getReflectedPointAboutLine(start, end, controlPoint2)
val controlP1X: Float
val controlP1Y: Float
val controlP2X: Float
val controlP2Y: Float
val path = Path()
path.moveTo(startX, startY)
if (reflected) {
controlP1X = reflectedControlPoint1.x
controlP1Y = reflectedControlPoint1.y
controlP2X = reflectedControlPoint2.x
controlP2Y = reflectedControlPoint2.y
} else {
controlP1X = controlPoint1.x
controlP1Y = controlPoint1.y
controlP2X = controlPoint2.x
controlP2Y = controlPoint2.y
}
path.cubicTo(controlP1X, controlP1Y, controlP2X, controlP2Y, endX, endY)
return path
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment