Added proportion_from_point method to VMobject, which uses some new bezier curve utilities (#1469)

* Add function to get the bezier curve parameter given a point and the bezier's control points.
Add function to determine if a point lies on a bezier curve.

* Add method to get_nth_curve_length
Add method to get the proportion along the path of the VMobject a particular point is.

* Added type hints and docstrings to added bezier functions.
Added better docstrings to added VMobject methods.
Ran black on both files.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Apply some of @friedkeenan 's suggestions

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix an incorrect import.

* Changed root filter to be more readable.
Removed CLOSED_THRESHOLD as it was unused everywhere else in the library
Added round_to parameter to replace CLOSED_THRESHOLD
Set default value of round_to to 1e-6, which is the same as VMobject.tolerance_for_point_equality

* Simplify polynomial generating method.

* Make bezier_params_from_point work for bezier curves of any degree.

* More descriptive names for k and l

* Apply suggestions from @friedkeenan's code review

Formatting changes; code cleanliness.
Made `point_lies_on_bezier_curve` pass `round_to` to `bezier_params_from_point`

Co-authored-by: friedkeenan <friedkeenan@protonmail.com>

* Formatting changes as suggested by @friedkeenan.

* Rename bezier_params_from_point to proportions_along_bezier_curve_for_point

Use proportions_along_bezier_curve_for_point alone and not point_lies_on_bezier in proportion_from_point

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Use proper typings for proportions_along_bezier_curve_for_point

Co-authored-by: friedkeenan <friedkeenan@protonmail.com>

* Add a comment paragraph explaining how proportion_from_point works.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix a bug where certain points are wrongly identified as being on the curve.

Thanks @Skaft !

* Moved around and added explanation comments.

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: friedkeenan <friedkeenan@protonmail.com>
This commit is contained in:
Aathish Sivasubrahmanian 2021-09-03 10:59:01 +05:30 committed by GitHub
commit c11b649e9a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 202 additions and 2 deletions

View file

@ -1917,6 +1917,9 @@ class Mobject:
def point_from_proportion(self, alpha):
raise NotImplementedError("Please override in a child class.")
def proportion_from_point(self, point):
raise NotImplementedError("Please override in a child class.")
def get_pieces(self, n_pieces):
template = self.copy()
template.submobjects = []

View file

@ -30,6 +30,8 @@ from ...utils.bezier import (
integer_interpolate,
interpolate,
partial_bezier_points,
point_lies_on_bezier,
proportions_along_bezier_curve_for_point,
)
from ...utils.color import BLACK, WHITE, color_to_rgba
from ...utils.deprecation import deprecated, deprecated_params
@ -1030,6 +1032,28 @@ class VMobject(Mobject):
"""
return bezier(self.get_nth_curve_points(n))
def get_nth_curve_length(
self, n: int, sample_points: Optional[int] = None
) -> float:
"""Returns the (approximate) length of the nth curve.
Parameters
----------
n
The index of the desired curve.
sample_points
The number of points to sample to find the length.
Returns
-------
length : :class:`float`
The length of the nth curve.
"""
_, length = self.get_nth_curve_function_with_length(n, sample_points)
return length
def get_nth_curve_function_with_length(
self, n: int, sample_points: Optional[int] = None
) -> typing.Tuple[typing.Callable[[float], np.ndarray], float]:
@ -1155,6 +1179,61 @@ class VMobject(Mobject):
current_length += length
def proportion_from_point(
self,
point: typing.Iterable[typing.Union[float, int]],
) -> float:
"""Returns the proportion along the path of the :class:`VMobject`
a particular given point is at.
Parameters
----------
point
The Cartesian coordinates of the point which may or may not lie on the :class:`VMobject`
Returns
-------
float
The proportion along the path of the :class:`VMobject`.
Raises
------
:exc:`ValueError`
If ``point`` does not lie on the curve.
:exc:`Exception`
If the :class:`VMobject` has no points.
"""
self.throw_error_if_no_points()
# Iterate over each bezier curve that the ``VMobject`` is composed of, checking
# if the point lies on that curve. If it does not lie on that curve, add
# the whole length of the curve to ``target_length`` and move onto the next
# curve. If the point does lie on the curve, add how far along the curve
# the point is to ``target_length``.
# Then, divide ``target_length`` by the total arc length of the shape to get
# the proportion along the ``VMobject`` the point is at.
num_curves = self.get_num_curves()
total_length = self.get_arc_length()
target_length = 0
for n in range(num_curves):
control_points = self.get_nth_curve_points(n)
length = self.get_nth_curve_length(n)
proportions_along_bezier = proportions_along_bezier_curve_for_point(
point, control_points
)
if len(proportions_along_bezier) > 0:
proportion_along_nth_curve = max(proportions_along_bezier)
target_length += length * proportion_along_nth_curve
break
target_length += length
else:
raise ValueError(f"Point {point} does not lie on this curve.")
alpha = target_length / total_length
return alpha
def get_anchors_and_handles(self) -> typing.Iterable[np.ndarray]:
"""Returns anchors1, handles1, handles2, anchors2,
where (anchors1[i], handles1[i], handles2[i], anchors2[i])

View file

@ -13,10 +13,13 @@ __all__ = [
"get_smooth_cubic_bezier_handle_points",
"diag_to_matrix",
"is_closed",
"proportions_along_bezier_curve_for_point",
"point_lies_on_bezier",
]
import typing
from functools import reduce
import numpy as np
from scipy import linalg
@ -24,8 +27,6 @@ from scipy import linalg
from ..utils.simple_functions import choose
from ..utils.space_ops import cross2d, find_intersection
CLOSED_THRESHOLD: float = 0.001
def bezier(
points: np.ndarray,
@ -367,3 +368,120 @@ def get_quadratic_approximation_of_cubic(a0, h0, h1, a1):
def is_closed(points: typing.Tuple[np.ndarray, np.ndarray]) -> bool:
return np.allclose(points[0], points[-1])
def proportions_along_bezier_curve_for_point(
point: typing.Iterable[typing.Union[float, int]],
control_points: typing.Iterable[typing.Iterable[typing.Union[float, int]]],
round_to: typing.Optional[typing.Union[float, int]] = 1e-6,
) -> np.ndarray:
"""Obtains the proportion along the bezier curve corresponding to a given point
given the bezier curve's control points.
The bezier polynomial is constructed using the coordinates of the given point
as well as the bezier curve's control points. On solving the polynomial for each dimension,
if there are roots common to every dimension, those roots give the proportion along the
curve the point is at. If there are no real roots, the point does not lie on the curve.
Parameters
----------
point
The Cartesian Coordinates of the point whose parameter
should be obtained.
control_points
The Cartesian Coordinates of the ordered control
points of the bezier curve on which the point may
or may not lie.
round_to
A float whose number of decimal places all values
such as coordinates of points will be rounded.
Returns
-------
np.ndarray[float]
List containing possible parameters (the proportions along the bezier curve)
for the given point on the given bezier curve.
This usually only contains one or zero elements, but if the
point is, say, at the beginning/end of a closed loop, may return
a list with more than 1 value, corresponding to the beginning and
end etc. of the loop.
Raises
------
:class:`ValueError`
When ``point`` and the control points have different shapes.
"""
# Method taken from
# http://polymathprogrammer.com/2012/04/03/does-point-lie-on-bezier-curve/
if not all(np.shape(point) == np.shape(c_p) for c_p in control_points):
raise ValueError(
f"Point {point} and Control Points {control_points} have different shapes."
)
control_points = np.array(control_points)
n = len(control_points) - 1
roots = []
for dim, coord in enumerate(point):
control_coords = control_points[:, dim]
terms = []
for term_power in range(n, -1, -1):
outercoeff = choose(n, term_power)
term = []
sign = 1
for subterm_num in range(term_power, -1, -1):
innercoeff = choose(term_power, subterm_num) * sign
subterm = innercoeff * control_coords[subterm_num]
if term_power == 0:
subterm -= coord
term.append(subterm)
sign *= -1
terms.append(outercoeff * sum(np.array(term)))
if all(term == 0 for term in terms):
# Then both Bezier curve and Point lie on the same plane.
# Roots will be none, but in this specific instance, we don't need to consider that.
continue
bezier_polynom = np.polynomial.Polynomial(terms[::-1])
polynom_roots = bezier_polynom.roots()
if len(polynom_roots) > 0:
polynom_roots = np.around(polynom_roots, int(np.log10(1 / round_to)))
roots.append(polynom_roots)
roots = [[root for root in rootlist if root.imag == 0] for rootlist in roots]
roots = reduce(np.intersect1d, roots) # Get common roots.
roots = np.array([r.real for r in roots])
return roots
def point_lies_on_bezier(
point: typing.Iterable[typing.Union[float, int]],
control_points: typing.Iterable[typing.Iterable[typing.Union[float, int]]],
round_to: typing.Optional[typing.Union[float, int]] = 1e-6,
) -> bool:
"""Checks if a given point lies on the bezier curves with the given control points.
This is done by solving the bezier polynomial with the point as the constant term; if
any real roots exist, the point lies on the bezier curve.
Parameters
----------
point
The Cartesian Coordinates of the point to check.
control_points
The Cartesian Coordinates of the ordered control
points of the bezier curve on which the point may
or may not lie.
round_to
A float whose number of decimal places all values
such as coordinates of points will be rounded.
Returns
-------
bool
Whether the point lies on the curve.
"""
roots = proportions_along_bezier_curve_for_point(point, control_points, round_to)
return len(roots) > 0