codex/control-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()