codexcontrol-point-cubic-curve-edit.py
control-point-cubic-curve-edit.py
"""Draw a cubic control-polygon edit; save the matching PNG in the CWD.
Tested with Python 3.14, numpy 2.3.5 and matplotlib 3.10.7.
Run from _media with MPLBACKEND=Agg.
"""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
def curve(controls, t):
t = np.asarray(t)
weights = np.stack(((1-t)**3, 3*t*(1-t)**2,
3*t**2*(1-t), t**3), axis=-1)
return weights @ controls
def main():
plt.rcParams.update({"font.size": 11, "figure.facecolor": "white",
"axes.facecolor": "white"})
blue, orange = "#17689b", "#bb4f12"
controls = np.array(((0., 0.), (1., 2.), (3., 2.), (4., 0.)))
edited = controls.copy()
edited[1] += (0, 1)
t = np.linspace(0, 1, 601)
before, after = curve(controls, t), curve(edited, t)
fig, axes = plt.subplots(1, 2, figsize=(10.8, 5.3), layout="constrained")
for ax in axes:
ax.set(xlim=(-.4, 4.4), ylim=(-.4, 3.65), xlabel="$x$", ylabel="$y$")
ax.set_aspect("equal")
ax.set_xticks(range(5))
ax.set_yticks(range(4))
ax.grid(alpha=.18)
for side in ("top", "right"):
ax.spines[side].set_visible(False)
left, right = axes
left.set_title("Before: four handles define the curve", pad=12)
left.plot(*controls.T, "o--", color=blue, lw=1.2, ms=7,
mfc="white", label="Control polygon")
left.plot(*before.T, color=blue, lw=2.6, label="Original curve")
offsets = ((-13, -23), (-23, 12), (-16, 12), (-5, -23))
for i, (p, offset) in enumerate(zip(controls, offsets)):
left.annotate(f"$P_{i}$", p, xytext=offset, textcoords="offset points")
mid = curve(controls, .5)
left.plot(*mid, "o", color=blue, ms=5)
left.annotate("$C(1/2)=(2,1.5)$", mid, xytext=(10, -35),
textcoords="offset points", arrowprops={"arrowstyle": "-"})
left.legend(loc="upper right", frameon=False)
right.set_title("After: move only $P_1$ by $h=(0,1)$", pad=12)
right.plot(*before.T, color=blue, lw=2, alpha=.8, label="Original curve")
right.plot(*after.T, color=orange, lw=2.6, label="Edited curve")
right.plot(*edited.T, "o--", color=orange, lw=1.2, ms=7, mfc="white")
right.plot(*controls[1], "o", color=blue, ms=6, mfc="white")
right.annotate("", edited[1], controls[1],
arrowprops={"arrowstyle": "->", "color": "#333333", "lw": 1.6})
right.annotate("$h$", (.88, 2.5), ha="right")
right.annotate("$P_1=(1,2)$", controls[1], xytext=(-38, 5),
textcoords="offset points", ha="right", fontsize=10)
right.annotate(r"$\widetilde P_1=(1,3)$", edited[1], xytext=(-45, 14),
textcoords="offset points", fontsize=10)
for i in (0, 2, 3):
right.annotate(f"$P_{i}$", edited[i], xytext=offsets[i],
textcoords="offset points")
a, b = curve(controls, 1/3), curve(edited, 1/3)
right.plot(*np.array((a, b)).T, "o", color="#333333", ms=4)
right.annotate("", b, a, arrowprops={"arrowstyle": "->", "lw": 1.6})
right.annotate(r"$\Delta C(1/3)=(0,4/9)$", (a+b)/2,
xytext=(2.05, .65), fontsize=10,
arrowprops={"arrowstyle": "-", "color": "#666666"})
right.legend(loc="upper right", frameon=False, fontsize=10)
fig.suptitle("Moving a control point reshapes a curve", fontsize=16)
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=100,
facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()