# ---- 基矢方向与标注 ----
def p_xyz(theta, phi):
"""球面点 P (r=1)。"""
return (np.sin(theta) * np.cos(phi),
np.sin(theta) * np.sin(phi),
np.cos(theta))
def basis_spherical(theta, phi):
"""球基矢单位方向(θ 从 +z 轴量起、φ 从 x 轴量起)。"""
rhat = (np.sin(theta) * np.cos(phi),
np.sin(theta) * np.sin(phi),
np.cos(theta))
thhat = (np.cos(theta) * np.cos(phi),
np.cos(theta) * np.sin(phi),
-np.sin(theta))
phat = (-np.sin(phi), np.cos(phi), 0.0)
return rhat, thhat, phat
xhat = (1.0, 0.0, 0.0)
yhat = (0.0, 1.0, 0.0)
zhat = (0.0, 0.0, 1.0)
dynamic = [] # 每次重绘产生的 artist,更新前移除
def draw(theta, phi):
for a in dynamic:
a.remove()
dynamic.clear()
px, py, pz = p_xyz(theta, phi)
pt = ax.scatter([px], [py], [pz], color='black', s=40, depthshade=False)
dynamic.append(pt)
# 原点 O 标记与 OP 连线(灰色虚线,示意位置向量)
origin = ax.scatter([0], [0], [0], color='dimgray', s=30, depthshade=False)
dynamic.append(origin)
op_line = ax.plot([0, px], [0, py], [0, pz], color='dimgray', linestyle='--')[0]
dynamic.append(op_line)
rhat, thhat, phat = basis_spherical(theta, phi)
# 球基矢:统一红色实线
q1 = ax.quiver([px] * 3, [py] * 3, [pz] * 3,
[rhat[0], thhat[0], phat[0]],
[rhat[1], thhat[1], phat[1]],
[rhat[2], thhat[2], phat[2]],
length=0.5, arrow_length_ratio=0.2, pivot='tail',
color='tab:red')
dynamic.append(q1)
# 直角基矢:统一蓝色虚线(颜色+线型双编码,色盲友好)
q2 = ax.quiver([px] * 3, [py] * 3, [pz] * 3,
[xhat[0], yhat[0], zhat[0]],
[xhat[1], yhat[1], zhat[1]],
[xhat[2], yhat[2], zhat[2]],
length=0.5, arrow_length_ratio=0.2, pivot='tail',
color='tab:blue', linestyle='--')
dynamic.append(q2)
# 六个基矢符号标签
labels = [r'$\hat{r}$', r'$\hat{\theta}$', r'$\hat{\phi}$',
r'$\hat{x}$', r'$\hat{y}$', r'$\hat{z}$']
dirs = [rhat, thhat, phat, xhat, yhat, zhat]
for lab, (dx, dy, dz) in zip(labels, dirs):
t = ax.text(px + 0.7 * dx, py + 0.7 * dy, pz + 0.7 * dz,
lab, fontsize=12)
dynamic.append(t)
# θ 弧:φ 所在经线平面内,从 +z 轴到 r 方向
alpha = np.linspace(0, theta, 30)
arc_theta = ax.plot(0.35 * np.sin(alpha) * np.cos(phi),
0.35 * np.sin(alpha) * np.sin(phi),
0.35 * np.cos(alpha),
color='black')[0]
dynamic.append(arc_theta)
am = 0.5 * theta
t_theta = ax.text(0.35 * 1.5 * np.sin(am) * np.cos(phi),
0.35 * 1.5 * np.sin(am) * np.sin(phi),
0.35 * 1.5 * np.cos(am),
r'$\theta$', fontsize=12)
dynamic.append(t_theta)
# φ 弧:xy 平面内,从 x 轴到投影方向
beta = np.linspace(0, phi, 30)
arc_phi = ax.plot(0.35 * np.cos(beta), 0.35 * np.sin(beta),
np.zeros_like(beta), color='dimgray')[0]
dynamic.append(arc_phi)
bm = 0.5 * phi
t_phi = ax.text(0.35 * 1.5 * np.cos(bm), 0.35 * 1.5 * np.sin(bm), 0,
r'$\phi$', fontsize=12)
dynamic.append(t_phi)
# 视角随 φ 转动:azim = φ + 视角偏移(s_azim_off 滑块调节,默认 35°),
# 错开正对方向从斜侧面观察,避免"起点在球心"错觉;elev 不变 ⇒ 不随 θ 转动
ax.view_init(azim=np.degrees(phi) + np.degrees(s_azim_off.val))
fig.canvas.draw_idle()
# ---- Slider 与事件绑定 ----
plt.subplots_adjust(bottom=0.42)
ax_azim_slider = plt.axes([0.2, 0.30, 0.6, 0.03])
ax_theta_slider = plt.axes([0.2, 0.20, 0.6, 0.03])
ax_phi_slider = plt.axes([0.2, 0.10, 0.6, 0.03])
s_azim_off = Slider(ax_azim_slider, r'$\phi_{\rm off}$', -np.pi, np.pi, valinit=np.radians(35))
s_theta = Slider(ax_theta_slider, r'$\theta$', 0, np.pi, valinit=np.pi / 4)
s_phi = Slider(ax_phi_slider, r'$\phi$', 0, 2 * np.pi, valinit=np.pi / 4)
s_theta.on_changed(lambda v: draw(v, s_phi.val))
s_phi.on_changed(lambda v: draw(s_theta.val, v))
s_azim_off.on_changed(lambda v: draw(s_theta.val, s_phi.val))
# θ=0/π、φ=0 时弧退化为点,零长线可正常绘制,无需特判;
# θ=π/2、φ=0 时 r̂ 与 x̂ 重合属几何必然
draw(np.pi / 4, np.pi / 4)
plt.show()