Created
October 27, 2019 09:59
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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