|
28 | 28 |
|
29 | 29 | from manim.typing import PointDType
|
30 | 30 | from manim.utils.simple_functions import choose
|
31 |
| -from manim.utils.space_ops import cross2d, find_intersection |
32 | 31 |
|
33 | 32 | if TYPE_CHECKING:
|
34 | 33 | import numpy.typing as npt
|
|
39 | 38 | MatrixMN,
|
40 | 39 | Point3D,
|
41 | 40 | Point3D_Array,
|
| 41 | + QuadraticBezierPoints_Array, |
42 | 42 | )
|
43 | 43 |
|
44 | 44 | # l is a commonly used name in linear algebra
|
@@ -1610,74 +1610,147 @@ def get_smooth_open_cubic_bezier_handle_points(
|
1610 | 1610 | return H1, H2
|
1611 | 1611 |
|
1612 | 1612 |
|
1613 |
| -# Given 4 control points for a cubic bezier curve (or arrays of such) |
1614 |
| -# return control points for 2 quadratics (or 2n quadratics) approximating them. |
| 1613 | +@overload |
1615 | 1614 | def get_quadratic_approximation_of_cubic(
|
1616 | 1615 | a0: Point3D, h0: Point3D, h1: Point3D, a1: Point3D
|
1617 |
| -) -> BezierPoints: |
1618 |
| - a0 = np.array(a0, ndmin=2) |
1619 |
| - h0 = np.array(h0, ndmin=2) |
1620 |
| - h1 = np.array(h1, ndmin=2) |
1621 |
| - a1 = np.array(a1, ndmin=2) |
1622 |
| - # Tangent vectors at the start and end. |
1623 |
| - T0 = h0 - a0 |
1624 |
| - T1 = a1 - h1 |
1625 |
| - |
1626 |
| - # Search for inflection points. If none are found, use the |
1627 |
| - # midpoint as a cut point. |
1628 |
| - # Based on http://www.caffeineowl.com/graphics/2d/vectorial/cubic-inflexion.html |
1629 |
| - has_infl = np.ones(len(a0), dtype=bool) |
1630 |
| - |
1631 |
| - p = h0 - a0 |
1632 |
| - q = h1 - 2 * h0 + a0 |
1633 |
| - r = a1 - 3 * h1 + 3 * h0 - a0 |
1634 |
| - |
1635 |
| - a = cross2d(q, r) |
1636 |
| - b = cross2d(p, r) |
1637 |
| - c = cross2d(p, q) |
1638 |
| - |
1639 |
| - disc = b * b - 4 * a * c |
1640 |
| - has_infl &= disc > 0 |
1641 |
| - sqrt_disc = np.sqrt(np.abs(disc)) |
1642 |
| - settings = np.seterr(all="ignore") |
1643 |
| - ti_bounds = [] |
1644 |
| - for sgn in [-1, +1]: |
1645 |
| - ti = (-b + sgn * sqrt_disc) / (2 * a) |
1646 |
| - ti[a == 0] = (-c / b)[a == 0] |
1647 |
| - ti[(a == 0) & (b == 0)] = 0 |
1648 |
| - ti_bounds.append(ti) |
1649 |
| - ti_min, ti_max = ti_bounds |
1650 |
| - np.seterr(**settings) |
1651 |
| - ti_min_in_range = has_infl & (0 < ti_min) & (ti_min < 1) |
1652 |
| - ti_max_in_range = has_infl & (0 < ti_max) & (ti_max < 1) |
1653 |
| - |
1654 |
| - # Choose a value of t which starts at 0.5, |
1655 |
| - # but is updated to one of the inflection points |
1656 |
| - # if they lie between 0 and 1 |
1657 |
| - |
1658 |
| - t_mid = 0.5 * np.ones(len(a0)) |
1659 |
| - t_mid[ti_min_in_range] = ti_min[ti_min_in_range] |
1660 |
| - t_mid[ti_max_in_range] = ti_max[ti_max_in_range] |
1661 |
| - |
1662 |
| - m, n = a0.shape |
1663 |
| - t_mid = t_mid.repeat(n).reshape((m, n)) |
1664 |
| - |
1665 |
| - # Compute bezier point and tangent at the chosen value of t (these are vectorized) |
1666 |
| - mid = bezier([a0, h0, h1, a1])(t_mid) # type: ignore |
1667 |
| - Tm = bezier([h0 - a0, h1 - h0, a1 - h1])(t_mid) # type: ignore |
1668 |
| - |
1669 |
| - # Intersection between tangent lines at end points |
1670 |
| - # and tangent in the middle |
1671 |
| - i0 = find_intersection(a0, T0, mid, Tm) |
1672 |
| - i1 = find_intersection(a1, T1, mid, Tm) |
1673 |
| - |
1674 |
| - m, n = np.shape(a0) |
1675 |
| - result = np.zeros((6 * m, n)) |
| 1616 | +) -> QuadraticBezierPoints_Array: ... |
| 1617 | + |
| 1618 | + |
| 1619 | +@overload |
| 1620 | +def get_quadratic_approximation_of_cubic( |
| 1621 | + a0: Point3D_Array, |
| 1622 | + h0: Point3D_Array, |
| 1623 | + h1: Point3D_Array, |
| 1624 | + a1: Point3D_Array, |
| 1625 | +) -> QuadraticBezierPoints_Array: ... |
| 1626 | + |
| 1627 | + |
| 1628 | +def get_quadratic_approximation_of_cubic(a0, h0, h1, a1): |
| 1629 | + r"""If ``a0``, ``h0``, ``h1`` and ``a1`` are the control points of a cubic |
| 1630 | + Bézier curve, approximate the curve with two quadratic Bézier curves and |
| 1631 | + return an array of 6 points, where the first 3 points represent the first |
| 1632 | + quadratic curve and the last 3 represent the second one. |
| 1633 | +
|
| 1634 | + Otherwise, if ``a0``, ``h0``, ``h1`` and ``a1`` are _arrays_ of :math:`N` |
| 1635 | + points representing :math:`N` cubic Bézier curves, return an array of |
| 1636 | + :math:`6N` points where each group of :math:`6` consecutive points |
| 1637 | + approximates each of the :math:`N` curves in a similar way as above. |
| 1638 | +
|
| 1639 | + .. note:: |
| 1640 | + If the cubic spline given by the original cubic Bézier curves is |
| 1641 | + smooth, this algorithm will generate a quadratic spline which is also |
| 1642 | + smooth. |
| 1643 | +
|
| 1644 | + If a cubic Bézier is given by |
| 1645 | +
|
| 1646 | + .. math:: |
| 1647 | + C(t) = (1-t)^3 A_0 + 3(1-t)^2 t H_0 + 3(1-t)t^2 H_1 + t^3 A_1 |
| 1648 | +
|
| 1649 | + where :math:`A_0`, :math:`H_0`, :math:`H_1` and :math:`A_1` are its |
| 1650 | + control points, then this algorithm should generate two quadratic |
| 1651 | + Béziers given by |
| 1652 | +
|
| 1653 | + .. math:: |
| 1654 | + Q_0(t) &= (1-t)^2 A_0 + 2(1-t)t M_0 + t^2 K \\ |
| 1655 | + Q_1(t) &= (1-t)^2 K + 2(1-t)t M_1 + t^2 A_1 |
| 1656 | +
|
| 1657 | + where :math:`M_0` and :math:`M_1` are the respective handles to be |
| 1658 | + found for both curves, and :math:`K` is the end anchor of the 1st curve |
| 1659 | + and the start anchor of the 2nd, which must also be found. |
| 1660 | +
|
| 1661 | + To solve for :math:`M_0`, :math:`M_1` and :math:`K`, three conditions |
| 1662 | + can be imposed: |
| 1663 | +
|
| 1664 | + 1. :math:`Q_0'(0) = \frac{1}{2}C'(0)`. The derivative of the first |
| 1665 | + quadratic curve at :math:`t = 0` should be proportional to that of |
| 1666 | + the original cubic curve, also at :math:`t = 0`. Because the cubic |
| 1667 | + curve is split into two parts, it is necessary to divide this by |
| 1668 | + two: the speed of a point travelling through the curve should be |
| 1669 | + half of the original. This gives: |
| 1670 | +
|
| 1671 | + .. math:: |
| 1672 | + Q_0'(0) &= \frac{1}{2}C'(0) \\ |
| 1673 | + 2(M_0 - A_0) &= \frac{3}{2}(H_0 - A_0) \\ |
| 1674 | + 2M_0 - 2A_0 &= \frac{3}{2}H_0 - \frac{3}{2}A_0 \\ |
| 1675 | + 2M_0 &= \frac{3}{2}H_0 + \frac{1}{2}A_0 \\ |
| 1676 | + M_0 &= \frac{1}{4}(3H_0 + A_0) |
| 1677 | +
|
| 1678 | + 2. :math:`Q_1'(1) = \frac{1}{2}C'(1)`. The derivative of the second |
| 1679 | + quadratic curve at :math:`t = 1` should be half of that of the |
| 1680 | + original cubic curve for the same reasons as above, also at |
| 1681 | + :math:`t = 1`. This gives: |
| 1682 | +
|
| 1683 | + .. math:: |
| 1684 | + Q_1'(1) &= \frac{1}{2}C'(1) \\ |
| 1685 | + 2(A_1 - M_1) &= \frac{3}{2}(A_1 - H_1) \\ |
| 1686 | + 2A_1 - 2M_1 &= \frac{3}{2}A_1 - \frac{3}{2}H_1 \\ |
| 1687 | + -2M_1 &= -\frac{1}{2}A_1 - \frac{3}{2}H_1 \\ |
| 1688 | + M_1 &= \frac{1}{4}(3H_1 + A_1) |
| 1689 | +
|
| 1690 | + 3. :math:`Q_0'(1) = Q_1'(0)`. The derivatives of both quadratic curves |
| 1691 | + should match at the point :math:`K`, in order for the final spline |
| 1692 | + to be smooth. This gives: |
| 1693 | +
|
| 1694 | + .. math:: |
| 1695 | + Q_0'(1) &= Q_1'(0) \\ |
| 1696 | + 2(K - M_0) &= 2(M_1 - K) \\ |
| 1697 | + 2K - 2M_0 &= 2M_1 - 2K \\ |
| 1698 | + 4K &= 2M_0 + 2M_1 \\ |
| 1699 | + K &= \frac{1}{2}(M_0 + M_1) |
| 1700 | +
|
| 1701 | + This is sufficient to find proper control points for the quadratic |
| 1702 | + Bézier curves. |
| 1703 | +
|
| 1704 | + Parameters |
| 1705 | + ---------- |
| 1706 | + a0 |
| 1707 | + The start anchor of a single cubic Bézier curve, or an array of |
| 1708 | + :math:`N` start anchors for :math:`N` curves. |
| 1709 | + h0 |
| 1710 | + The first handle of a single cubic Bézier curve, or an array of |
| 1711 | + :math:`N` first handles for :math:`N` curves. |
| 1712 | + h1 |
| 1713 | + The second handle of a single cubic Bézier curve, or an array of |
| 1714 | + :math:`N` second handles for :math:`N` curves. |
| 1715 | + a1 |
| 1716 | + The end anchor of a single cubic Bézier curve, or an array of |
| 1717 | + :math:`N` end anchors for :math:`N` curves. |
| 1718 | +
|
| 1719 | + Returns |
| 1720 | + ------- |
| 1721 | + result |
| 1722 | + An array containing either 6 points for 2 quadratic Bézier curves |
| 1723 | + approximating the original cubic curve, or :math:`6N` points for |
| 1724 | + :math:`2N` quadratic curves approximating :math:`N` cubic curves. |
| 1725 | +
|
| 1726 | + Raises |
| 1727 | + ------ |
| 1728 | + ValueError |
| 1729 | + If ``a0``, ``h0``, ``h1`` and ``a1`` have different dimensions, or |
| 1730 | + if their number of dimensions is not 1 or 2. |
| 1731 | + """ |
| 1732 | + a0 = np.asarray(a0) |
| 1733 | + h0 = np.asarray(h0) |
| 1734 | + h1 = np.asarray(h1) |
| 1735 | + a1 = np.asarray(a1) |
| 1736 | + |
| 1737 | + if all(arr.ndim == 1 for arr in (a0, h0, h1, a1)): |
| 1738 | + num_curves, dim = 1, a0.shape[0] |
| 1739 | + elif all(arr.ndim == 2 for arr in (a0, h0, h1, a1)): |
| 1740 | + num_curves, dim = a0.shape |
| 1741 | + else: |
| 1742 | + raise ValueError("All arguments must be Point3D or Point3D_Array.") |
| 1743 | + |
| 1744 | + m0 = 0.25 * (3 * h0 + a0) |
| 1745 | + m1 = 0.25 * (3 * h1 + a1) |
| 1746 | + k = 0.5 * (m0 + m1) |
| 1747 | + |
| 1748 | + result = np.empty((6 * num_curves, dim)) |
1676 | 1749 | result[0::6] = a0
|
1677 |
| - result[1::6] = i0 |
1678 |
| - result[2::6] = mid |
1679 |
| - result[3::6] = mid |
1680 |
| - result[4::6] = i1 |
| 1750 | + result[1::6] = m0 |
| 1751 | + result[2::6] = k |
| 1752 | + result[3::6] = k |
| 1753 | + result[4::6] = m1 |
1681 | 1754 | result[5::6] = a1
|
1682 | 1755 | return result
|
1683 | 1756 |
|
|
0 commit comments