Last active
February 27, 2025 21:29
-
-
Save ColinMoldenhauer/485c3496effb351af8a07ed4a278e8ac to your computer and use it in GitHub Desktop.
LaTeX TikZ Bézier curves of arbitrary degree
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import string | |
from math import comb | |
def bernstein(n, i, var="t"): | |
"""Return the Bernstein polynomial term as a string.""" | |
return f"{comb(n, i)}*(1-{var})^({n-i})*{var}^({i})" | |
def generate_bezier_latex( | |
nodes, | |
samples=100, | |
domain=(0, 1), | |
width="10cm", | |
height="10cm", | |
anchors={}, | |
use_commands=True, | |
var="t" | |
): | |
r""" | |
Generates LaTeX TikZ code for an arbitrary Bézier curve given an array of control points. | |
TikZ natively supports Bézier curves until 3rd degree (start, end, 2 control points): | |
``` | |
\draw(p0)..controls (p1) and (p2)..(p3); | |
``` | |
Any higher order Bézier curve needs to be computed and drawn manually. | |
$$B(t) = \sum_{i=0}^{n} B_i^n(t) P_i$$ | |
with | |
$$B_i^n(t) = \binom{n}{i} (1 - t)^{n-i} t^i$$ | |
Parameters | |
---------- | |
nodes : np.ndarray or list | |
- if `np.ndarray` array of shape (2, N), as used as input for python package `bezier` | |
- if `list` list of coordinate tuples, i.e. `[(x0, y0), ..., (xN, yN)]` | |
samples : int | |
At how many points to evaluate the Bézier curve | |
domain : tuple | |
2-tuple indicating start and end value of curve parameter `t` | |
width : str | |
Width of plot | |
height : str | |
Height of plot | |
anchors : dict, optional | |
Dictionary mapping index of control point `i` to the anchor position for the point label | |
use_commands : bool, optional | |
- if `True` | |
generate a macro for each control point coordinate and insert the macro at coordinate locations. Useful to manually change the coordinates without rerunning this function | |
- if `False` | |
insert the node coordinate values directly at coordinate locations | |
var : str | |
Variable name for Bézier parameter | |
Returns | |
------- | |
str | |
LaTeX TikZ code drawing an arbitrary Bézier curve | |
""" | |
if isinstance(nodes, np.ndarray): | |
coords = [(x_, y_) for x_, y_ in nodes.T] | |
else: | |
coords = nodes | |
n = len(coords) - 1 # degree of the Bézier curve | |
indent = "\t" | |
def get_id(idx, alphabet=string.ascii_uppercase): | |
"""Get a unique coordinate ID by cycling `alphabet`.""" | |
result = "" | |
idx += 1 | |
while idx > 0: | |
idx, remainder = divmod(idx - 1, len(alphabet)) | |
result += alphabet[remainder] | |
return result[::-1] | |
def format_coordinate(i, x=None, y=None): | |
""" | |
Format a coordinate either by inserting its value `x` or `y` or | |
by inserting the coordinate's macro. | |
""" | |
assert (x is None) ^ (y is None) | |
if use_commands: | |
coord = "X" if x is not None else "Y" | |
return f"\\bezier{coord}{get_id(i)}" | |
else: | |
return x or y | |
# define control point values and coordinates as macros | |
if use_commands: | |
s_def = f"{indent}% Define control point coordinates as commands\n" | |
for i, (x, y) in enumerate(coords): | |
namex = format_coordinate(i, x=x) | |
namey = format_coordinate(i, y=y) | |
s_def += f"{indent}\\def{namex}{{{x}}}\n" | |
s_def += f"{indent}\\def{namey}{{{y}}}\n" | |
else: | |
s_def = "" | |
# Build the expressions for the x(t) and y(t) components: | |
terms_x = [] | |
terms_y = [] | |
for i, (x, y) in enumerate(coords): | |
terms_x.append(f"({bernstein(n, i, var)})*({format_coordinate(i, x=x)})") | |
terms_y.append(f"({bernstein(n, i, var)})*({format_coordinate(i, y=y)})") | |
bezier_x = " + ".join(terms_x) | |
bezier_y = " + ".join(terms_y) | |
# Generate code for plotting the control points: | |
control_points_code = "\n".join( | |
f"{indent}{indent}\\addplot[only marks, mark=*, red] coordinates {{({format_coordinate(i, x=x)},{format_coordinate(i, y=y)})}};" | |
for i, (x, y) in enumerate(coords) | |
) | |
# Generate code for labeling the control points: | |
control_labels_code = "\n".join( | |
f"{indent}{indent}\\node[anchor={anchors.get(i, 'north east')}] at ({format_coordinate(i, x=x)},{format_coordinate(i, y=y)}) {{\\texttt{{P{i}}} ({format_coordinate(i, x=x)},{format_coordinate(i, y=y)})}};" | |
for i, (x, y) in enumerate(coords) | |
) | |
# Build the full LaTeX code using pgfplots | |
latex_code = f"""\\documentclass{{article}} | |
\\usepackage{{pgfplots}} | |
\\pgfplotsset{{compat=1.17}} | |
\\begin{{document}} | |
\\begin{{tikzpicture}} | |
{s_def} | |
\\begin{{axis}}[ | |
axis lines = middle, | |
xlabel = {{\\(x\\)}}, | |
ylabel = {{\\(y\\)}}, | |
width={width}, | |
height={height}, | |
samples={samples}, | |
domain={domain[0]}:{domain[1]}, | |
variable={var} | |
] | |
% Plot the Bézier curve | |
\\addplot[ | |
thick, blue | |
] ( | |
{{ {bezier_x} }}, | |
{{ {bezier_y} }} | |
); | |
% Plot control points | |
{control_points_code} | |
% Label control points | |
{control_labels_code} | |
\\end{{axis}} | |
\\end{{tikzpicture}} | |
\\end{{document}} | |
""" | |
return latex_code | |
# Example usage: | |
control_points = np.round(np.random.rand(2, 10), 2) | |
latex_code = generate_bezier_latex(control_points) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Output:
summary