2022-02-12 23:47:23 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2022-02-13 15:18:04 +08:00
|
|
|
from typing import Iterable, Callable, TypeVar, Sequence
|
2022-02-12 23:47:23 +08:00
|
|
|
|
2018-12-24 12:37:51 -08:00
|
|
|
from scipy import linalg
|
2018-03-30 18:19:23 -07:00
|
|
|
import numpy as np
|
2022-02-13 15:18:04 +08:00
|
|
|
import numpy.typing as npt
|
2018-03-31 15:11:35 -07:00
|
|
|
|
2019-02-05 11:02:15 -08:00
|
|
|
from manimlib.utils.simple_functions import choose
|
2020-02-04 15:25:08 -08:00
|
|
|
from manimlib.utils.space_ops import find_intersection
|
2020-06-08 20:40:39 -07:00
|
|
|
from manimlib.utils.space_ops import cross2d
|
2021-08-21 17:07:49 -07:00
|
|
|
from manimlib.utils.space_ops import midpoint
|
2021-10-07 17:37:10 +08:00
|
|
|
from manimlib.logger import log
|
2018-03-30 18:19:23 -07:00
|
|
|
|
2018-04-17 15:01:25 -07:00
|
|
|
CLOSED_THRESHOLD = 0.001
|
2022-02-12 23:47:23 +08:00
|
|
|
T = TypeVar("T")
|
2018-03-30 18:19:23 -07:00
|
|
|
|
2022-02-13 12:56:03 +08:00
|
|
|
def bezier(
|
|
|
|
points: Iterable[float | np.ndarray]
|
|
|
|
) -> Callable[[float], float | np.ndarray]:
|
2018-03-30 18:19:23 -07:00
|
|
|
n = len(points) - 1
|
2020-06-27 12:10:22 -07:00
|
|
|
|
|
|
|
def result(t):
|
2022-02-13 15:16:16 -08:00
|
|
|
return sum(
|
2020-06-27 12:10:22 -07:00
|
|
|
((1 - t)**(n - k)) * (t**k) * choose(n, k) * point
|
|
|
|
for k, point in enumerate(points)
|
2022-02-13 15:16:16 -08:00
|
|
|
)
|
2020-06-27 12:10:22 -07:00
|
|
|
|
|
|
|
return result
|
2018-03-30 18:19:23 -07:00
|
|
|
|
2018-04-06 13:58:59 -07:00
|
|
|
|
2022-02-12 23:47:23 +08:00
|
|
|
def partial_bezier_points(
|
2022-02-13 15:18:04 +08:00
|
|
|
points: Sequence[np.ndarray],
|
2022-02-12 23:47:23 +08:00
|
|
|
a: float,
|
|
|
|
b: float
|
|
|
|
) -> list[float]:
|
2018-03-30 18:19:23 -07:00
|
|
|
"""
|
2020-02-12 11:49:16 -08:00
|
|
|
Given an list of points which define
|
2018-03-30 18:19:23 -07:00
|
|
|
a bezier curve, and two numbers 0<=a<b<=1,
|
2020-02-12 11:49:16 -08:00
|
|
|
return an list of the same size, which
|
2018-03-30 18:19:23 -07:00
|
|
|
describes the portion of the original bezier
|
|
|
|
curve on the interval [a, b].
|
|
|
|
|
|
|
|
This algorithm is pretty nifty, and pretty dense.
|
|
|
|
"""
|
2019-02-05 15:25:44 -08:00
|
|
|
if a == 1:
|
|
|
|
return [points[-1]] * len(points)
|
|
|
|
|
2020-02-12 11:49:16 -08:00
|
|
|
a_to_1 = [
|
2018-03-30 18:19:23 -07:00
|
|
|
bezier(points[i:])(a)
|
|
|
|
for i in range(len(points))
|
2020-02-12 11:49:16 -08:00
|
|
|
]
|
2019-02-05 15:25:44 -08:00
|
|
|
end_prop = (b - a) / (1. - a)
|
2020-02-12 11:49:16 -08:00
|
|
|
return [
|
2019-02-05 15:25:44 -08:00
|
|
|
bezier(a_to_1[:i + 1])(end_prop)
|
2018-03-30 18:19:23 -07:00
|
|
|
for i in range(len(points))
|
2020-02-12 11:49:16 -08:00
|
|
|
]
|
2018-03-30 18:19:23 -07:00
|
|
|
|
|
|
|
|
2020-06-27 12:10:22 -07:00
|
|
|
# Shortened version of partial_bezier_points just for quadratics,
|
|
|
|
# since this is called a fair amount
|
2022-02-12 23:47:23 +08:00
|
|
|
def partial_quadratic_bezier_points(
|
2022-02-13 15:18:04 +08:00
|
|
|
points: Sequence[np.ndarray],
|
2022-02-12 23:47:23 +08:00
|
|
|
a: float,
|
|
|
|
b: float
|
|
|
|
) -> list[float]:
|
2020-06-27 12:17:41 -07:00
|
|
|
if a == 1:
|
|
|
|
return 3 * [points[-1]]
|
|
|
|
|
2020-06-27 12:10:22 -07:00
|
|
|
def curve(t):
|
|
|
|
return points[0] * (1 - t) * (1 - t) + 2 * points[1] * t * (1 - t) + points[2] * t * t
|
|
|
|
# bezier(points)
|
|
|
|
h0 = curve(a) if a > 0 else points[0]
|
|
|
|
h2 = curve(b) if b < 1 else points[2]
|
|
|
|
h1_prime = (1 - a) * points[1] + a * points[2]
|
|
|
|
end_prop = (b - a) / (1. - a)
|
|
|
|
h1 = (1 - end_prop) * h0 + end_prop * h1_prime
|
|
|
|
return [h0, h1, h2]
|
|
|
|
|
|
|
|
|
2018-03-30 18:19:23 -07:00
|
|
|
# Linear interpolation variants
|
|
|
|
|
2022-02-12 23:47:23 +08:00
|
|
|
def interpolate(start: T, end: T, alpha: float) -> T:
|
2021-01-10 18:51:47 -08:00
|
|
|
try:
|
|
|
|
return (1 - alpha) * start + alpha * end
|
|
|
|
except TypeError:
|
2021-10-16 13:04:52 +08:00
|
|
|
log.debug(f"`start` parameter with type `{type(start)}` and dtype `{start.dtype}`")
|
|
|
|
log.debug(f"`end` parameter with type `{type(end)}` and dtype `{end.dtype}`")
|
|
|
|
log.debug(f"`alpha` parameter with value `{alpha}`")
|
2021-01-10 18:51:47 -08:00
|
|
|
import sys
|
|
|
|
sys.exit(2)
|
2018-04-06 13:58:59 -07:00
|
|
|
|
2018-03-30 18:19:23 -07:00
|
|
|
|
2022-02-12 23:47:23 +08:00
|
|
|
def set_array_by_interpolation(
|
2022-02-15 11:35:22 +08:00
|
|
|
arr: np.ndarray,
|
|
|
|
arr1: np.ndarray,
|
|
|
|
arr2: np.ndarray,
|
2022-02-12 23:47:23 +08:00
|
|
|
alpha: float,
|
2022-02-15 11:35:22 +08:00
|
|
|
interp_func: Callable[[np.ndarray, np.ndarray, float], np.ndarray] = interpolate
|
|
|
|
) -> np.ndarray:
|
2021-01-11 10:57:23 -10:00
|
|
|
arr[:] = interp_func(arr1, arr2, alpha)
|
2020-06-28 12:15:01 -07:00
|
|
|
return arr
|
|
|
|
|
|
|
|
|
2022-02-12 23:47:23 +08:00
|
|
|
def integer_interpolate(
|
|
|
|
start: T,
|
|
|
|
end: T,
|
|
|
|
alpha: float
|
|
|
|
) -> tuple[int, float]:
|
2019-02-05 11:02:15 -08:00
|
|
|
"""
|
|
|
|
alpha is a float between 0 and 1. This returns
|
|
|
|
an integer between start and end (inclusive) representing
|
|
|
|
appropriate interpolation between them, along with a
|
|
|
|
"residue" representing a new proportion between the
|
|
|
|
returned integer and the next one of the
|
|
|
|
list.
|
|
|
|
|
|
|
|
For example, if start=0, end=10, alpha=0.46, This
|
|
|
|
would return (4, 0.6).
|
|
|
|
"""
|
|
|
|
if alpha >= 1:
|
|
|
|
return (end - 1, 1.0)
|
2019-02-05 15:25:44 -08:00
|
|
|
if alpha <= 0:
|
|
|
|
return (start, 0)
|
2019-02-05 11:02:15 -08:00
|
|
|
value = int(interpolate(start, end, alpha))
|
|
|
|
residue = ((end - start) * alpha) % 1
|
|
|
|
return (value, residue)
|
|
|
|
|
|
|
|
|
2022-02-12 23:47:23 +08:00
|
|
|
def mid(start: T, end: T) -> T:
|
2018-04-06 13:58:59 -07:00
|
|
|
return (start + end) / 2.0
|
|
|
|
|
2018-03-30 18:19:23 -07:00
|
|
|
|
2022-02-12 23:47:23 +08:00
|
|
|
def inverse_interpolate(start: T, end: T, value: T) -> float:
|
2018-03-30 18:19:23 -07:00
|
|
|
return np.true_divide(value - start, end - start)
|
|
|
|
|
2018-04-06 13:58:59 -07:00
|
|
|
|
2022-02-12 23:47:23 +08:00
|
|
|
def match_interpolate(
|
|
|
|
new_start: T,
|
|
|
|
new_end: T,
|
|
|
|
old_start: T,
|
|
|
|
old_end: T,
|
|
|
|
old_value: T
|
|
|
|
) -> T:
|
2018-03-30 18:19:23 -07:00
|
|
|
return interpolate(
|
2018-04-06 13:58:59 -07:00
|
|
|
new_start, new_end,
|
2018-03-30 18:19:23 -07:00
|
|
|
inverse_interpolate(old_start, old_end, old_value)
|
|
|
|
)
|
|
|
|
|
2018-04-06 13:58:59 -07:00
|
|
|
|
2022-02-12 23:47:23 +08:00
|
|
|
def get_smooth_quadratic_bezier_handle_points(
|
2022-02-13 15:18:04 +08:00
|
|
|
points: Sequence[np.ndarray]
|
2022-02-12 23:47:23 +08:00
|
|
|
) -> np.ndarray | list[np.ndarray]:
|
2021-02-05 15:25:17 -08:00
|
|
|
"""
|
|
|
|
Figuring out which bezier curves most smoothly connect a sequence of points.
|
|
|
|
|
|
|
|
Given three successive points, P0, P1 and P2, you can compute that by defining
|
|
|
|
h = (1/4) P0 + P1 - (1/4)P2, the bezier curve defined by (P0, h, P1) will pass
|
|
|
|
through the point P2.
|
|
|
|
|
|
|
|
So for a given set of four successive points, P0, P1, P2, P3, if we want to add
|
|
|
|
a handle point h between P1 and P2 so that the quadratic bezier (P1, h, P2) is
|
|
|
|
part of a smooth curve passing through all four points, we calculate one solution
|
|
|
|
for h that would produce a parbola passing through P3, call it smooth_to_right, and
|
|
|
|
another that would produce a parabola passing through P0, call it smooth_to_left,
|
|
|
|
and use the midpoint between the two.
|
|
|
|
"""
|
2021-08-21 17:07:49 -07:00
|
|
|
if len(points) == 2:
|
|
|
|
return midpoint(*points)
|
2021-02-05 15:25:17 -08:00
|
|
|
smooth_to_right, smooth_to_left = [
|
|
|
|
0.25 * ps[0:-2] + ps[1:-1] - 0.25 * ps[2:]
|
|
|
|
for ps in (points, points[::-1])
|
|
|
|
]
|
2021-02-05 16:29:07 -08:00
|
|
|
if np.isclose(points[0], points[-1]).all():
|
|
|
|
last_str = 0.25 * points[-2] + points[-1] - 0.25 * points[1]
|
|
|
|
last_stl = 0.25 * points[1] + points[0] - 0.25 * points[-2]
|
|
|
|
else:
|
|
|
|
last_str = smooth_to_left[0]
|
|
|
|
last_stl = smooth_to_right[0]
|
|
|
|
handles = 0.5 * np.vstack([smooth_to_right, [last_str]])
|
|
|
|
handles += 0.5 * np.vstack([last_stl, smooth_to_left[::-1]])
|
2021-02-05 15:25:17 -08:00
|
|
|
return handles
|
2018-03-30 18:19:23 -07:00
|
|
|
|
2018-04-06 13:58:59 -07:00
|
|
|
|
2022-02-12 23:47:23 +08:00
|
|
|
def get_smooth_cubic_bezier_handle_points(
|
2022-02-13 15:18:04 +08:00
|
|
|
points: npt.ArrayLike
|
2022-02-12 23:47:23 +08:00
|
|
|
) -> tuple[np.ndarray, np.ndarray]:
|
2018-03-30 18:19:23 -07:00
|
|
|
points = np.array(points)
|
|
|
|
num_handles = len(points) - 1
|
2018-04-06 13:58:59 -07:00
|
|
|
dim = points.shape[1]
|
2018-03-30 18:19:23 -07:00
|
|
|
if num_handles < 1:
|
|
|
|
return np.zeros((0, dim)), np.zeros((0, dim))
|
2018-04-06 13:58:59 -07:00
|
|
|
# Must solve 2*num_handles equations to get the handles.
|
|
|
|
# l and u are the number of lower an upper diagonal rows
|
|
|
|
# in the matrix to solve.
|
|
|
|
l, u = 2, 1
|
|
|
|
# diag is a representation of the matrix in diagonal form
|
|
|
|
# See https://www.particleincell.com/2012/bezier-splines/
|
2021-08-07 22:25:26 +07:00
|
|
|
# for how to arrive at these equations
|
2018-04-06 13:58:59 -07:00
|
|
|
diag = np.zeros((l + u + 1, 2 * num_handles))
|
|
|
|
diag[0, 1::2] = -1
|
|
|
|
diag[0, 2::2] = 1
|
|
|
|
diag[1, 0::2] = 2
|
|
|
|
diag[1, 1::2] = 1
|
|
|
|
diag[2, 1:-2:2] = -2
|
|
|
|
diag[3, 0:-3:2] = 1
|
|
|
|
# last
|
|
|
|
diag[2, -2] = -1
|
|
|
|
diag[1, -1] = 2
|
|
|
|
# This is the b as in Ax = b, where we are solving for x,
|
|
|
|
# and A is represented using diag. However, think of entries
|
|
|
|
# to x and b as being points in space, not numbers
|
|
|
|
b = np.zeros((2 * num_handles, dim))
|
|
|
|
b[1::2] = 2 * points[1:]
|
2018-03-30 18:19:23 -07:00
|
|
|
b[0] = points[0]
|
|
|
|
b[-1] = points[-1]
|
2018-04-06 13:58:59 -07:00
|
|
|
|
|
|
|
def solve_func(b):
|
|
|
|
return linalg.solve_banded((l, u), diag, b)
|
2020-06-09 16:58:16 -07:00
|
|
|
|
2018-04-17 15:01:25 -07:00
|
|
|
use_closed_solve_function = is_closed(points)
|
|
|
|
if use_closed_solve_function:
|
2018-04-06 13:58:59 -07:00
|
|
|
# Get equations to relate first and last points
|
2018-03-30 18:19:23 -07:00
|
|
|
matrix = diag_to_matrix((l, u), diag)
|
2018-04-06 13:58:59 -07:00
|
|
|
# last row handles second derivative
|
2018-03-30 18:19:23 -07:00
|
|
|
matrix[-1, [0, 1, -2, -1]] = [2, -1, 1, -2]
|
2018-04-06 13:58:59 -07:00
|
|
|
# first row handles first derivative
|
|
|
|
matrix[0, :] = np.zeros(matrix.shape[1])
|
|
|
|
matrix[0, [0, -1]] = [1, 1]
|
|
|
|
b[0] = 2 * points[0]
|
2018-03-30 18:19:23 -07:00
|
|
|
b[-1] = np.zeros(dim)
|
2018-04-06 13:58:59 -07:00
|
|
|
|
2018-04-17 15:01:25 -07:00
|
|
|
def closed_curve_solve_func(b):
|
2018-04-06 13:58:59 -07:00
|
|
|
return linalg.solve(matrix, b)
|
2018-04-17 15:01:25 -07:00
|
|
|
|
2018-04-06 13:58:59 -07:00
|
|
|
handle_pairs = np.zeros((2 * num_handles, dim))
|
2018-03-30 18:19:23 -07:00
|
|
|
for i in range(dim):
|
2018-04-17 15:01:25 -07:00
|
|
|
if use_closed_solve_function:
|
|
|
|
handle_pairs[:, i] = closed_curve_solve_func(b[:, i])
|
|
|
|
else:
|
|
|
|
handle_pairs[:, i] = solve_func(b[:, i])
|
2018-03-30 18:19:23 -07:00
|
|
|
return handle_pairs[0::2], handle_pairs[1::2]
|
|
|
|
|
2018-04-06 13:58:59 -07:00
|
|
|
|
2022-02-12 23:47:23 +08:00
|
|
|
def diag_to_matrix(
|
|
|
|
l_and_u: tuple[int, int],
|
|
|
|
diag: np.ndarray
|
|
|
|
) -> np.ndarray:
|
2018-03-30 18:19:23 -07:00
|
|
|
"""
|
2018-04-06 13:58:59 -07:00
|
|
|
Converts array whose rows represent diagonal
|
2018-03-30 18:19:23 -07:00
|
|
|
entries of a matrix into the matrix itself.
|
|
|
|
See scipy.linalg.solve_banded
|
|
|
|
"""
|
|
|
|
l, u = l_and_u
|
|
|
|
dim = diag.shape[1]
|
|
|
|
matrix = np.zeros((dim, dim))
|
2018-04-06 13:58:59 -07:00
|
|
|
for i in range(l + u + 1):
|
2018-03-30 18:19:23 -07:00
|
|
|
np.fill_diagonal(
|
2018-04-06 13:58:59 -07:00
|
|
|
matrix[max(0, i - u):, max(0, u - i):],
|
|
|
|
diag[i, max(0, u - i):]
|
2018-03-30 18:19:23 -07:00
|
|
|
)
|
|
|
|
return matrix
|
|
|
|
|
2018-04-06 13:58:59 -07:00
|
|
|
|
2022-02-13 15:18:04 +08:00
|
|
|
def is_closed(points: Sequence[np.ndarray]) -> bool:
|
2019-02-05 11:02:15 -08:00
|
|
|
return np.allclose(points[0], points[-1])
|
2020-02-04 15:25:08 -08:00
|
|
|
|
|
|
|
|
|
|
|
# Given 4 control points for a cubic bezier curve (or arrays of such)
|
|
|
|
# return control points for 2 quadratics (or 2n quadratics) approximating them.
|
2022-02-12 23:47:23 +08:00
|
|
|
def get_quadratic_approximation_of_cubic(
|
2022-02-13 15:18:04 +08:00
|
|
|
a0: npt.ArrayLike,
|
|
|
|
h0: npt.ArrayLike,
|
|
|
|
h1: npt.ArrayLike,
|
|
|
|
a1: npt.ArrayLike
|
2022-02-12 23:47:23 +08:00
|
|
|
) -> np.ndarray:
|
2020-02-04 15:25:08 -08:00
|
|
|
a0 = np.array(a0, ndmin=2)
|
|
|
|
h0 = np.array(h0, ndmin=2)
|
|
|
|
h1 = np.array(h1, ndmin=2)
|
|
|
|
a1 = np.array(a1, ndmin=2)
|
2020-02-06 10:01:19 -08:00
|
|
|
# Tangent vectors at the start and end.
|
2020-02-04 15:25:08 -08:00
|
|
|
T0 = h0 - a0
|
|
|
|
T1 = a1 - h1
|
|
|
|
|
2020-02-06 10:01:19 -08:00
|
|
|
# Search for inflection points. If none are found, use the
|
|
|
|
# midpoint as a cut point.
|
|
|
|
# Based on http://www.caffeineowl.com/graphics/2d/vectorial/cubic-inflexion.html
|
|
|
|
has_infl = np.ones(len(a0), dtype=bool)
|
|
|
|
|
|
|
|
p = h0 - a0
|
|
|
|
q = h1 - 2 * h0 + a0
|
|
|
|
r = a1 - 3 * h1 + 3 * h0 - a0
|
|
|
|
|
|
|
|
a = cross2d(q, r)
|
|
|
|
b = cross2d(p, r)
|
|
|
|
c = cross2d(p, q)
|
|
|
|
|
|
|
|
disc = b * b - 4 * a * c
|
|
|
|
has_infl &= (disc > 0)
|
2020-06-08 20:40:39 -07:00
|
|
|
sqrt_disc = np.sqrt(np.abs(disc))
|
2020-02-07 09:31:06 -08:00
|
|
|
settings = np.seterr(all='ignore')
|
2020-06-08 20:40:39 -07:00
|
|
|
ti_bounds = []
|
|
|
|
for sgn in [-1, +1]:
|
|
|
|
ti = (-b + sgn * sqrt_disc) / (2 * a)
|
|
|
|
ti[a == 0] = (-c / b)[a == 0]
|
|
|
|
ti[(a == 0) & (b == 0)] = 0
|
|
|
|
ti_bounds.append(ti)
|
|
|
|
ti_min, ti_max = ti_bounds
|
2020-02-07 09:31:06 -08:00
|
|
|
np.seterr(**settings)
|
2020-02-12 12:53:21 -08:00
|
|
|
ti_min_in_range = has_infl & (0 < ti_min) & (ti_min < 1)
|
|
|
|
ti_max_in_range = has_infl & (0 < ti_max) & (ti_max < 1)
|
2020-02-06 10:01:19 -08:00
|
|
|
|
2021-02-05 15:25:17 -08:00
|
|
|
# Choose a value of t which starts at 0.5,
|
2020-02-06 10:01:19 -08:00
|
|
|
# but is updated to one of the inflection points
|
|
|
|
# if they lie between 0 and 1
|
|
|
|
|
|
|
|
t_mid = 0.5 * np.ones(len(a0))
|
2020-02-07 09:31:06 -08:00
|
|
|
t_mid[ti_min_in_range] = ti_min[ti_min_in_range]
|
|
|
|
t_mid[ti_max_in_range] = ti_max[ti_max_in_range]
|
2020-02-06 10:01:19 -08:00
|
|
|
|
|
|
|
m, n = a0.shape
|
|
|
|
t_mid = t_mid.repeat(n).reshape((m, n))
|
|
|
|
|
|
|
|
# Compute bezier point and tangent at the chosen value of t
|
|
|
|
mid = bezier([a0, h0, h1, a1])(t_mid)
|
|
|
|
Tm = bezier([h0 - a0, h1 - h0, a1 - h1])(t_mid)
|
|
|
|
|
|
|
|
# Intersection between tangent lines at end points
|
|
|
|
# and tangent in the middle
|
2020-02-04 15:25:08 -08:00
|
|
|
i0 = find_intersection(a0, T0, mid, Tm)
|
|
|
|
i1 = find_intersection(a1, T1, mid, Tm)
|
|
|
|
|
|
|
|
m, n = np.shape(a0)
|
|
|
|
result = np.zeros((6 * m, n))
|
|
|
|
result[0::6] = a0
|
|
|
|
result[1::6] = i0
|
|
|
|
result[2::6] = mid
|
|
|
|
result[3::6] = mid
|
|
|
|
result[4::6] = i1
|
|
|
|
result[5::6] = a1
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
2022-02-12 23:47:23 +08:00
|
|
|
def get_smooth_quadratic_bezier_path_through(
|
|
|
|
points: list[np.ndarray]
|
|
|
|
) -> np.ndarray:
|
2020-06-09 12:38:37 -07:00
|
|
|
# TODO
|
|
|
|
h0, h1 = get_smooth_cubic_bezier_handle_points(points)
|
2020-02-04 15:25:08 -08:00
|
|
|
a0 = points[:-1]
|
|
|
|
a1 = points[1:]
|
|
|
|
return get_quadratic_approximation_of_cubic(a0, h0, h1, a1)
|