先看一段“有毛病”的代码直观感受一下问题from manim import * import numpy as np class BadRoseCurve(Scene): def construct(self): axes Axes(x_range[-3, 3], y_range[-3, 3]) self.add(axes) # 用累积密度法生成非均匀 theta尖端稀疏(快)中部密集(慢) n_points 500 t np.linspace(0, 2 * PI, n_points) # 密度函数在 cos(5t)0 处(花瓣中部)密度高在 |cos(5t)|1 处(尖端)密度低 density 1 3 * np.sin(5 * t) ** 2 theta np.cumsum(density) theta theta / theta[-1] * PI # 归一化到 [0, π]k为奇数时只需π即可画完 r np.cos(5 * theta) x r * 2 * np.cos(theta) y r * 2 * np.sin(theta) points [axes.c2p(x[i], y[i]) for i in range(n_points)] curve VMobject(colorPINK, stroke_width3) curve.set_points_as_corners(points) # 用 Create 画曲线——速度明显不均匀 self.play(Create(curve), run_time5, rate_funclinear) self.wait()运行这个动画你会看到花瓣尖端几帧就画完了而花瓣中部却画得很慢。rate_funclinear控制的是动画进度的均匀但曲线本身点的分布就是疏密不均的所以视觉速度必然忽快忽慢。2. SymPy 解决方案弧长参数化三步走要让绘制速度均匀核心思路是生成一组让相邻点之间实际距离相等的参数值。具体分三步用积分算出弧长函数 L(θ)——从起点到参数 θ 的弧长算总弧长等分出一组目标弧长值 s1,s2,...对每个$ s_i 反解方程 L(\theta)s_i 得到对应的 \theta_i $这三步SymPy都能帮上忙。2.1 用 SymPy 推导弧长函数import sympy as sp theta sp.Symbol(theta, realTrue) n 5 # 五瓣玫瑰线 # 极坐标方程注意这里放大了2倍与痛点代码中的 r*2 保持一致 r 2 * sp.cos(n * theta) # 极坐标 → 直角坐标符号推导零误差 x r * sp.cos(theta) # x r(θ)·cos(θ) y r * sp.sin(theta) # y r(θ)·sin(θ) # 弧长微元ds/dθ sqrt((dx/dθ)² (dy/dθ)²) dx_dtheta sp.diff(x, theta) dy_dtheta sp.diff(y, theta) # 弧长微元表达式注意这里不算积分只保留被积函数 ds_dtheta sp.sqrt(dx_dtheta**2 dy_dtheta**2) print(弧长微元 ds/dθ , sp.simplify(ds_dtheta)) # 运行结果 弧长微元 ds/dθ 2*sqrt((2*sin(4*theta) 3*sin(6*theta))**2 (2*cos(4*theta) - 3*cos(6*theta))**2) SymPy会输出一个椭圆积分形式的表达式——这是正常的很多曲线的弧长都没有初等表达式。没关系数值求解一样好用。2.2 用 nsolve 反解等弧长参数点import numpy as np from scipy.integrate import quad from scipy.optimize import bisect # 数值求根稳定且快 # 把弧长微元转成可数值计算的函数 ds_func sp.lambdify(theta, ds_dtheta, numpy) # 数值弧长函数L(t) ∫₀^t ds def arc_length(t): 计算从 0 到 t 的弧长 result, _ quad(ds_func, 0, t, limit100) return result # 计算总弧长 total_length arc_length(2 * np.pi) print(f总弧长: {total_length:.4f}) # 等分弧长用数值求根反解对应的 theta N 500 s_values np.linspace(0, total_length, N) theta_vals [] # 辅助函数f(t) L(t) - s我们要找 f(t)0 的根 def f(t, s): return arc_length(t) - s for i, s in enumerate(s_values): if i 0: theta_vals.append(0.0) # s0 对应 theta0 continue # 初始搜索区间用上一个 theta 作为左边界 # 弧长是单调递增的所以解一定在 [左边界, 右边界] 之间 left theta_vals[-1] # 上一个已解出的 theta right left 0.5 # 向右扩展足够覆盖下一个等分点 # 扩展右边界直到 f(right, s) 0确保根在区间内 while f(right, s) 0: right 0.5 # 二分法求根稳定、快速 sol bisect(f, left, right, args(s,), xtol1e-8) theta_vals.append(float(sol)) theta_vals np.array(theta_vals)代码要点sp.lambdify把符号表达式编译成 NumPy 函数求值快sp.nsolve数值解方程给定一个好猜测值能显著加速猜测值用“弧长占比 × 总参数范围”做线性估计足够接近真实解3. Manim 联动实战把上面的计算和 Manim 动画串起来就是一份完整可运行的代码from manim import * import numpy as np import sympy as sp from scipy.integrate import quad from scipy.optimize import bisect class UniformRoseCurve(Scene): def construct(self): # SymPy 数值积分计算等弧长参数点 theta sp.Symbol(theta, realTrue) n 5 # 极坐标方程 r 2*cos(5θ) r 2 * sp.cos(n * theta) # 极坐标转直角坐标 x_expr r * sp.cos(theta) y_expr r * sp.sin(theta) # 弧长微元 dx sp.diff(x_expr, theta) dy sp.diff(y_expr, theta) ds_dtheta sp.sqrt(dx**2 dy**2) # 数值弧长函数 ds_func sp.lambdify(theta, ds_dtheta, numpy) def arc_length(t): val, _ quad(ds_func, 0, t, limit100) return val # 总弧长k为奇数时只需π即可画完 total_len arc_length(np.pi) print(f总弧长: {total_len:.4f}) # 用二分法反解等弧长 theta速度快 N 500 s_vals np.linspace(0, total_len, N) theta_vals [0.0] def f(t, s): return arc_length(t) - s for i in range(1, N): s s_vals[i] left theta_vals[-1] right left 0.5 # 扩展右边界直到 f(right) 0 while f(right, s) 0: right 0.5 sol bisect(f, left, right, args(s,), xtol1e-8) theta_vals.append(float(sol)) theta_vals np.array(theta_vals) # 计算直角坐标点 x_func sp.lambdify(theta, x_expr, numpy)
痛点场景还原:一个具体的例子
发布时间:2026/6/30 6:13:47
先看一段“有毛病”的代码直观感受一下问题from manim import * import numpy as np class BadRoseCurve(Scene): def construct(self): axes Axes(x_range[-3, 3], y_range[-3, 3]) self.add(axes) # 用累积密度法生成非均匀 theta尖端稀疏(快)中部密集(慢) n_points 500 t np.linspace(0, 2 * PI, n_points) # 密度函数在 cos(5t)0 处(花瓣中部)密度高在 |cos(5t)|1 处(尖端)密度低 density 1 3 * np.sin(5 * t) ** 2 theta np.cumsum(density) theta theta / theta[-1] * PI # 归一化到 [0, π]k为奇数时只需π即可画完 r np.cos(5 * theta) x r * 2 * np.cos(theta) y r * 2 * np.sin(theta) points [axes.c2p(x[i], y[i]) for i in range(n_points)] curve VMobject(colorPINK, stroke_width3) curve.set_points_as_corners(points) # 用 Create 画曲线——速度明显不均匀 self.play(Create(curve), run_time5, rate_funclinear) self.wait()运行这个动画你会看到花瓣尖端几帧就画完了而花瓣中部却画得很慢。rate_funclinear控制的是动画进度的均匀但曲线本身点的分布就是疏密不均的所以视觉速度必然忽快忽慢。2. SymPy 解决方案弧长参数化三步走要让绘制速度均匀核心思路是生成一组让相邻点之间实际距离相等的参数值。具体分三步用积分算出弧长函数 L(θ)——从起点到参数 θ 的弧长算总弧长等分出一组目标弧长值 s1,s2,...对每个$ s_i 反解方程 L(\theta)s_i 得到对应的 \theta_i $这三步SymPy都能帮上忙。2.1 用 SymPy 推导弧长函数import sympy as sp theta sp.Symbol(theta, realTrue) n 5 # 五瓣玫瑰线 # 极坐标方程注意这里放大了2倍与痛点代码中的 r*2 保持一致 r 2 * sp.cos(n * theta) # 极坐标 → 直角坐标符号推导零误差 x r * sp.cos(theta) # x r(θ)·cos(θ) y r * sp.sin(theta) # y r(θ)·sin(θ) # 弧长微元ds/dθ sqrt((dx/dθ)² (dy/dθ)²) dx_dtheta sp.diff(x, theta) dy_dtheta sp.diff(y, theta) # 弧长微元表达式注意这里不算积分只保留被积函数 ds_dtheta sp.sqrt(dx_dtheta**2 dy_dtheta**2) print(弧长微元 ds/dθ , sp.simplify(ds_dtheta)) # 运行结果 弧长微元 ds/dθ 2*sqrt((2*sin(4*theta) 3*sin(6*theta))**2 (2*cos(4*theta) - 3*cos(6*theta))**2) SymPy会输出一个椭圆积分形式的表达式——这是正常的很多曲线的弧长都没有初等表达式。没关系数值求解一样好用。2.2 用 nsolve 反解等弧长参数点import numpy as np from scipy.integrate import quad from scipy.optimize import bisect # 数值求根稳定且快 # 把弧长微元转成可数值计算的函数 ds_func sp.lambdify(theta, ds_dtheta, numpy) # 数值弧长函数L(t) ∫₀^t ds def arc_length(t): 计算从 0 到 t 的弧长 result, _ quad(ds_func, 0, t, limit100) return result # 计算总弧长 total_length arc_length(2 * np.pi) print(f总弧长: {total_length:.4f}) # 等分弧长用数值求根反解对应的 theta N 500 s_values np.linspace(0, total_length, N) theta_vals [] # 辅助函数f(t) L(t) - s我们要找 f(t)0 的根 def f(t, s): return arc_length(t) - s for i, s in enumerate(s_values): if i 0: theta_vals.append(0.0) # s0 对应 theta0 continue # 初始搜索区间用上一个 theta 作为左边界 # 弧长是单调递增的所以解一定在 [左边界, 右边界] 之间 left theta_vals[-1] # 上一个已解出的 theta right left 0.5 # 向右扩展足够覆盖下一个等分点 # 扩展右边界直到 f(right, s) 0确保根在区间内 while f(right, s) 0: right 0.5 # 二分法求根稳定、快速 sol bisect(f, left, right, args(s,), xtol1e-8) theta_vals.append(float(sol)) theta_vals np.array(theta_vals)代码要点sp.lambdify把符号表达式编译成 NumPy 函数求值快sp.nsolve数值解方程给定一个好猜测值能显著加速猜测值用“弧长占比 × 总参数范围”做线性估计足够接近真实解3. Manim 联动实战把上面的计算和 Manim 动画串起来就是一份完整可运行的代码from manim import * import numpy as np import sympy as sp from scipy.integrate import quad from scipy.optimize import bisect class UniformRoseCurve(Scene): def construct(self): # SymPy 数值积分计算等弧长参数点 theta sp.Symbol(theta, realTrue) n 5 # 极坐标方程 r 2*cos(5θ) r 2 * sp.cos(n * theta) # 极坐标转直角坐标 x_expr r * sp.cos(theta) y_expr r * sp.sin(theta) # 弧长微元 dx sp.diff(x_expr, theta) dy sp.diff(y_expr, theta) ds_dtheta sp.sqrt(dx**2 dy**2) # 数值弧长函数 ds_func sp.lambdify(theta, ds_dtheta, numpy) def arc_length(t): val, _ quad(ds_func, 0, t, limit100) return val # 总弧长k为奇数时只需π即可画完 total_len arc_length(np.pi) print(f总弧长: {total_len:.4f}) # 用二分法反解等弧长 theta速度快 N 500 s_vals np.linspace(0, total_len, N) theta_vals [0.0] def f(t, s): return arc_length(t) - s for i in range(1, N): s s_vals[i] left theta_vals[-1] right left 0.5 # 扩展右边界直到 f(right) 0 while f(right, s) 0: right 0.5 sol bisect(f, left, right, args(s,), xtol1e-8) theta_vals.append(float(sol)) theta_vals np.array(theta_vals) # 计算直角坐标点 x_func sp.lambdify(theta, x_expr, numpy)