Update This is now available on CTAN (see above). Bugfixes and improvements will generally be available on github before being uploaded to CTAN. If getting the files from there, note that the relevant .dtx file is spath3.dtx
. This generates all the necessary files.
Update: I've added this to the TeX-SX package which was announced at What are your favourite TikZ/PGF answers?. The dtx
can be downloaded from http://bazaar.launchpad.net/+branch/tex-sx/files. Run pdflatex
on it to produce the sty
file and the documentation.
Hmm, I guess you're looking for something a bit like this:
It may not be easy, but I'll have a go ...
\documentclass{standalone}
\usepackage{tikz}
%%% TODO:
%
% 1. Be able to define a number of different pens and use appropriately
% 2. Add an annotation style (arrows with numbers)
% 3. Check how this interacts with colours and so forth
\makeatletter
\long\def\ge@addto@macro#1#2{%
\begingroup
\toks@\expandafter\expandafter\expandafter{\expandafter#1#2}%
\xdef#1{\the\toks@}%
\endgroup}
\long\def\ge@addbefore@macro#1#2{%
\begingroup
\toks@\expandafter\expandafter\expandafter{\expandafter#2#1}%
\xdef#1{\the\toks@}%
\endgroup}
\long\def\ge@addbefore@macro#1#2{%
\begingroup
\toks@\expandafter\expandafter\expandafter{\expandafter#2#1}%
\xdef#1{\the\toks@}%
\endgroup}
\long\def\g@addbefore@macro#1#2{%
\begingroup
\toks@\expandafter{\expandafter#2#1}%
\xdef#1{\the\toks@}%
\endgroup}
\pgfkeys{/tikz/store path in/.code={\pgfsyssoftpath@getcurrentpath{\my@path}\global\let#1=\my@path}}
\pgfkeys{/tikz/define pen/.style={preaction={store path in=\penpath}}}
\def\pen{\path[define pen]}
\def\calligraphy{\path[calligraphy]}
\pgfkeys{/tikz/calligraphy/.style={preaction={store path in=\calligraphypath},preaction={stroke with calligraphy pen=\penpath}}}
\pgfkeys{/tikz/stroke with calligraphy pen/.code={\thickenpath{\calligraphypath}{#1}}}
\def\thickenpartialpath#1#2{%
\def\thick@path{}%
\def\thick@action##1##2##3{%
\edef\this@action{\string##1}%
\ifx\this@action\cal@moveto
\ifx\thick@path\@empty
\else
\lengthofsoftpath{\thick@path}
\ifnum\value{softpathlength}=1
\thickenpartialpathwithstroke{#1}{\thick@path}
\else
\thickenpartialpathwithfill{#1}{\thick@path}
\fi
\def\thick@path{}%
\fi
\fi
\def\to@add{##1{##2}{##3}}
}
\let\thick@append=\ge@addto@macro
\expandafter\walksoftpath\expandafter\thick@path\expandafter\thick@action\expandafter\thick@append#2\relax
\lengthofsoftpath{\thick@path}
\ifnum\value{softpathlength}=1
\thickenpartialpathwithstroke{#1}{\thick@path}
\else
\thickenpartialpathwithfill{#1}{\thick@path}
\fi
}
\def\thickenpartialpathwithfill#1#2{%
\startsoftpath{#2}
\let\pen@sx=\tr@xlen
\let\pen@sy=\tr@ylen
\endsoftpath{#2}
\let\pen@ex=\tr@xlen
\let\pen@ey=\tr@ylen
\startsoftpath{#1}
\let\path@sx=\tr@xlen
\let\path@sy=\tr@ylen
\endsoftpath{#1}
\let\path@ex=\tr@xlen
\let\path@ey=\tr@ylen
\translatesoftpath{#1}{\pen@sx}{\pen@sy}%
\let\lower@path=\trpath
\translatesoftpath{#2}{\path@ex}{\path@ey}
\let\right@path=\trpath
\reversesoftpath{#1}
\translatesoftpath{\revpath}{\pen@ex}{\pen@ey}
\let\upper@path=\trpath
\reversesoftpath{#2}
\translatesoftpath{\revpath}{\path@sx}{\path@sy}
\let\left@path=\trpath
\catsoftpath{\lower@path}{\right@path}
\catsoftpath{\catpath}{\upper@path}
\catsoftpath{\catpath}{\left@path}
\def\to@add{\pgfsyssoftpath@closepath{0pt}{0pt}}
\ge@addto@macro\catpath\to@add
\pgfsyssoftpath@setcurrentpath{\catpath}
\pgfsyssoftpath@flushcurrentpath
\pgfusepath{fill}
}
\def\thickenpartialpathwithstroke#1#2{%
\startsoftpath{#2}
\let\pen@sx=\tr@xlen
\let\pen@sy=\tr@ylen
\translatesoftpath{#1}{\pen@sx}{\pen@sy}%
\pgfsyssoftpath@setcurrentpath{\trpath}
\pgfsyssoftpath@flushcurrentpath
\pgfusepath{stroke}
}
\def\catsoftpath#1#2{
\let\catpath=#1
\expandafter\trimfirst#2\relax
\ge@addto@macro\catpath\trimed@path
}
\def\trimfirst#1#2#3#4\relax{%
\edef\this@action{\string#1}%
\ifx\this@action\cal@moveto
\def\trimed@path{#4}%
\else
\def\trimed@path{#1{#2}{#3}#4}%
\fi
}
\def\walksoftpath#1#2#3#4{
\let\path@cmd=#4%
\ifx\path@cmd\relax
\let\next@action=\@gobblefour
\else
\let\next@action=\modifypath
\fi
\next@action{#1}{#2}{#3}{#4}%
}
\def\modifypath#1#2#3#4#5#6{%
#2{#4}{#5}{#6}
#3#1\to@add
\walksoftpath{#1}{#2}{#3}}
\newcounter{softpathlength}
\def\lengthofsoftpath#1{%
\def\len@path{}%
\setcounter{softpathlength}{0}%
\def\len@action##1##2##3{%
\stepcounter{softpathlength}%
\edef\to@add{}%
}
\let\len@append=\ge@addto@macro
\expandafter\walksoftpath\expandafter\len@path\expandafter\len@action\expandafter\len@append#1\relax
}
\def\translatesoftpath#1#2#3{%
\def\tr@path{}%
\def\tr@action##1##2##3{%
\pgfmathsetmacro{\tr@xlen}{##2+#2}
\pgfmathsetmacro{\tr@ylen}{##3+#3}
\edef\to@add{\noexpand##1{\tr@xlen pt}{\tr@ylen pt}}
}
\let\tr@append=\ge@addto@macro
\expandafter\walksoftpath\expandafter\tr@path\expandafter\tr@action\expandafter\tr@append#1\relax
\global\let\trpath=\tr@path
}
\def\trimlast#1\pgfsyssoftpath@movetotoken\relax{#1}
\foreach \cal@cpt in {
moveto,
lineto,
curvetosupporta,
curvetosupportb,
curveto,
rectcorner,
rectsize%
} {
\expandafter\xdef\csname cal@\cal@cpt\endcsname{\expandafter\string\csname pgfsyssoftpath@\cal@cpt token\endcsname}
}
\edef\cal@closepath{\string\pgfsyssoftpath@closepath}
\def\reversesoftpath#1{%
\def\re@path{}%
\def\re@action##1##2##3{%
\edef\this@re@action{\string##1}
\ifx\this@re@action\cal@curvetosupporta
\edef\to@add{\noexpand\pgfsyssoftpath@curvetosupportbtoken{##2}{##3}\noexpand\pgfsyssoftpath@curvetotoken}
\else
\ifx\this@re@action\cal@curvetosupportb
\edef\to@add{\noexpand\pgfsyssoftpath@curvetosupportatoken{##2}{##3}}
\else
\ifx\this@re@action\cal@curveto
\edef\to@add{{##2}{##3}}
\else
\edef\to@add{{##2}{##3}\noexpand##1}
\fi
\fi
\fi
}
\let\re@append=\ge@addbefore@macro
\expandafter\walksoftpath\expandafter\re@path\expandafter\re@action\expandafter\re@append#1\relax
\expandafter\expandafter\expandafter\def\expandafter\expandafter\expandafter\re@path\expandafter\expandafter\expandafter{\expandafter\trimlast\re@path\relax}
\g@addbefore@macro\re@path\pgfsyssoftpath@movetotoken
\global\let\revpath=\re@path
}
\def\startsoftpath#1{%
\def\st@path{}%
\def\st@action##1##2##3{%
\def\tr@xlen{##2}%
\def\tr@ylen{##3}%
\def\st@action####1####2####3{}
}
\let\st@append=\ge@addto@macro
\expandafter\walksoftpath\expandafter\st@path\expandafter\st@action\expandafter\st@append#1\relax
}
\def\endsoftpath#1{%
\def\end@path{}%
\def\end@action##1##2##3{%
\def\tr@xlen{##2}%
\def\tr@ylen{##3}%
}
\let\end@append=\ge@addto@macro
\expandafter\walksoftpath\expandafter\end@path\expandafter\end@action\expandafter\end@append#1\relax
}
\def\thickenpath#1#2{%
\def\th@path{}%
\def\th@action##1##2##3{%
\edef\this@action{\string##1}
\ifx\this@action\cal@moveto
\ifx\th@path\@empty
\else
\thickenpartialpath\th@path{#2}
\fi
\def\th@path{}
\fi
\def\to@add{##1{##2}{##3}}
}
\let\th@append=\ge@addto@macro
\expandafter\walksoftpath\expandafter\th@path\expandafter\th@action\expandafter\th@append#1\relax
\thickenpartialpath\th@path{#2}
}
\makeatother
\begin{document}
\begin{tikzpicture}
\pen (-.25,-.125) -- (0,0) ++(.125,.0625) -- (.25,.125);
\calligraphy (0,0) .. controls +(.5,.5) and +(-.5,-.5) .. ++(2,0) (1,0) -- ++(0,-2) -- ++(-.125,-.125) .. controls +(.1,.1) and +(-.1,-.1) .. ++(.35,0);
\calligraphy (3,-1) .. controls +(-1.5,1) and +(-1.5,-1) .. ++(0,-2) ++(-1.1,1) -- ++(1,0) -- ++(-.25,-.25);
\calligraphy (3.25,0) .. controls +(.5,.5) and +(-.5,-.5) .. ++(1.5,-2) ++(0,2.5) .. controls +(-.75,.75) and +(.75,-.75) .. ++(-1.5,-3);
\calligraphy (0,-4) .. controls +(1,1) and +(-1,-1) .. ++(5,0);
\end{tikzpicture}
\end{document}
First, the history. I enjoy calligraphy, I like producing documents in TeX, and lately with the addition of TikZ I've started enjoying producing graphical stuff so the combination of calligraphy with TikZ has been at the back of my mind for a long time. There have been a few times when reading a question here that I've though "I could do that with a calligraphy pen.". Also, when I found that Inkscape had a calligraphy tool then I thought it would be great in combination with a graphics tablet, but I was never very happy with the result - they weren't elegant enough to replace a hand-drawn one, nor precise enough to have the benefits of being computer-rendered (though being able to make a screencast can be useful: see this page for such a screencast, also by looking at the source of the SVGs you can see that Inkscape's method for doing calligraphy is ... not elegant.).
Anyway, I was idly perusing the PGF manual when I came across the concept of soft paths and realised that this was the key to doing calligraphy. Basically, (see the manual for more details) when TikZ/PGF constructs a path then it does so in several layers and at each step one can interrupt the process to mess with the data so far before passing it on to the next. One step is a soft path. This is the path in basic form, so once all coordinates have been computed and all funny paths rendered into their more basic components (so circles are replaced by bezier curves and so forth), but it is not yet "baked hard" so can still be manipulated.
The above script steps in at this point and says, "That path you've just constructed, rather than render it as is, I'm going to 'thicken' it.". In detail, it takes the path together with another path (called the "pen") and forms a (cartesian) product of the two. This then produces a region which it fills. The effect is as if the pen had been dragged along the path. By using quite complicated pens, quite complicated effects can be achieved.
Most of the time, this filling method produces exactly the desired effect. Where it fails is where the path turns a corner tangential to the pen path. If one were truly dragging a pen along the path, this corner would be filled, but because this method works by translation, that doesn't happen (an example in the picture is the top of the "X" on the right). However, when doing calligraphy, one should never push the pen and drawing a stroke where the path turns tangential to the pen would involve pushing the pen (so the off-diagonal of the "X" should really be three strokes: the main path and the two ends).
Note that once I've packaged this into something (and one reason for prompting Joseph to ask this question was to justify including it in the proposed "TikZ from TeX-SX" package!), the usage will be something like:
\pen (-.25,-.125) -- (0,0) ++(.125,.0625) -- (.25,.125);
\calligraphy (0,0) .. controls +(.5,.5) and +(-.5,-.5) .. ++(2,0) (1,0) -- ++(0,-2) -- ++(-.125,-.125) .. controls +(.1,.1) and +(-.1,-.1) .. ++(.35,0);
\calligraphy (3,-1) .. controls +(-1.5,1) and +(-1.5,-1) .. ++(0,-2) ++(-1.1,1) -- ++(1,0) -- ++(-.25,-.25);
\calligraphy (3.25,0) .. controls +(.5,.5) and +(-.5,-.5) .. ++(1.5,-2) ++(0,2.5) .. controls +(-.75,.75) and +(.75,-.75) .. ++(-1.5,-3);
\calligraphy (0,-4) .. controls +(1,1) and +(-1,-1) .. ++(5,0);
which is really quite simple.
Apart from the fun of "Hey, look what I just managed to do!", I also wanted to put this somewhere public because I think that this ability to manipulate the soft path is quite useful for other effects. It's something like decorations, but not quite, and the concepts that I've identified here will be useful whenever there is a situation where one wants to say, "That path there, I'd like to use it, but in a weird way.". In particular, doing something more than once with the same path, but modifying the path slightly for each action.
Just for fun, I decided to implement Hobby's algorithm in pure Python (well, not pure, I had to use numpy module to solve a linear system of equations).
Currently, my code works on simple paths, in which all joins are "curved" (i.e: "..") and no directions are specified at the knots. However, tension can be specified at each segment, and even as a "global" value to apply to the whole path. The path can be cyclic or open, and in the later it is also possible to specify initial and final curl.
The module can be called from LaTeX, using python.sty package or even better, using the technique demonstrated by Martin in another answer to this same question.
Adapting Martin's code to this case, the following example shows how to use the python script:
\documentclass{minimal}
\usepackage{tikz}
\usepackage{xparse}
\newcounter{mppath}
\DeclareDocumentCommand\mppath{ o m }{%
\addtocounter{mppath}{1}
\def\fname{path\themppath.tmp}
\IfNoValueTF{#1}
{\immediate\write18{python mp2tikz.py '#2' >\fname}}
{\immediate\write18{python mp2tikz.py '#2' '#1' >\fname}}
\input{\fname}
}
\begin{document}
\begin{tikzpicture}[scale=0.1]
\mppath[very thick]{(0,0)..(60,40)..tension 2..(40,90)..(10,70)..(30,50)..cycle}
\mppath[blue,tension=3]{(0,0)..(60,40)..(40,90)..(10,70)..(30,50)..cycle};
\end{tikzpicture}
\end{document}
Note that options passed to mppath are en general tikz options, but two new options are also available: tension
, which applies the given tension to all the path, and curl
which applies the given curl to both ends of an open path.
Running the above example through pdflatex -shell-escape
produces the following output:
The python code of this module is below. The details of the algorithm were obtained from the "METAFONT: The program" book. Currently the class design of the python code is prepared to deal with more complex kind of paths, but I did not have time to implement the part which breaks the path into "idependendty solvable" subpaths (this would be at knots which do not have smooth curvature, or at which the path changes from curved to straight). I tried to document the code as much as I can, so that anyone could improve it.
# mp2tikz.py
# (c) 2012 JL Diaz
#
# This module contains classes and functions to implement Jonh Hobby's
# algorithm to find a smooth curve which passes through a serie of given
# points. The algorithm is used in METAFONT and MetaPost, but the source code
# of these programs is hard to read. I tried to implement it in a more
# modern way, which makes the algorithm more understandandable and perhaps portable
# to other languages
#
# It can be imported as a python module in order to generate paths programatically
# or used from command line to convert a metapost path into a tikz one
#
# For the second case, the use is:
#
# $ python mp2tikz.py <metapost path> <options>
#
# Where:
# <metapost path> is a path using metapost syntax with the following restrictions:
# * All points have to be explicit (no variables or expressions)
# * All joins have to be "curved" ( .. operator)
# * Options in curly braces next to the nodes are ignored, except
# for {curl X} at end points
# * tension can be specified using metapost syntax
# * "cycle" as end point denotes a cyclic path, as in metapost
# Examples:
# (0,0) .. (60,40) .. (40,90) .. (10,70) .. (30,50) .. cycle
# (0,0) .. (60,40) .. (40,90) .. (10,70) .. (30,50)
# (0,0){curl 10} .. (60,40) .. (40,90) .. (10,70) .. (30,50)
# (0,0) .. (60,40) .. (40,90) .. tension 3 .. (10,70) .. (30,50) .. cycle
# (0,0) .. (60,40) .. (40,90) .. tension 1 and 3 .. (10,70) .. (30,50) .. cycle
#
# <options> can be:
# tension = X. The given tension is applied to all segments in the path by default
# (but tension given at specific points override this setting at those points)
# curl = X. The given curl is applied by default to both ends of the open path
# (but curl given at specific endings override this setting at that point)
# any other options are considered tikz options.
#
# The script prints in standard output a tikz command which draws the given path
# using the given options. In this path all control points are explicit, as computed
# by the string using Hobby's algorith.
#
# For example:
#
# $ python mp2tikz.py "(0,0) .. (10,10) .. (20,0) .. (10, -10) .. cycle" "tension =3, blue"
#
# Would produce
# \draw[blue] (0.0000, 0.0000) .. controls (-0.00000, 1.84095) and (8.15905, 10.00000)..
# (10.0000, 10.0000) .. controls (11.84095, 10.00000) and (20.00000, 1.84095)..
# (20.0000, 0.0000) .. controls (20.00000, -1.84095) and (11.84095, -10.00000)..
# (10.0000, -10.0000) .. controls (8.15905, -10.00000) and (0.00000, -1.84095)..(0.0000, 0.0000);
#
from math import sqrt, sin, cos, atan2, atan, degrees, radians, pi
# Coordinates are stored and manipulated as complex numbers,
# so we require cmath module
import cmath
def arg(z):
return atan2(z.imag, z.real)
def direc(angle):
"""Given an angle in degrees, returns a complex with modulo 1 and the
given phase"""
phi = radians(angle)
return complex(cos(phi), sin(phi))
def direc_rad(angle):
"""Given an angle in radians, returns a complex with modulo 1 and the
given phase"""
return complex(cos(angle), sin(angle))
class Point():
"""This class implements the coordinates of a knot, and all kind of
auxiliar parameters to compute a smooth path passing through it"""
z = complex(0,0) # Point coordinates
alpha = 1 # Tension at point (1 by default)
beta = 1
theta = 0 # Angle at which the path leaves
phi = 0 # Angle at which the path enters
xi = 0 # angle turned by the polyline at this point
v_left = complex(0,0) # Control points of the Bezier curve at this point
u_right = complex(0,0) # (to be computed later)
d_ant = 0 # Distance to previous point in the path
d_post = 0 # Distance to next point in the path
def __init__(self, z, alpha=1, beta=1, v=complex(0,0), u=complex(0,0)):
"""Constructor. Coordinates can be given as a complex number
or as a tuple (pair of reals). Remaining parameters are optional
and take sensible default vaules."""
if type(z)==complex:
self.z=z
else:
self.z=complex(z[0], z[1])
self.alpha = alpha
self.beta = beta
self.v_left = v
self.u_right = u
self.d_ant = 0
self.d_post = 0
self.xi = 0
def __str__(self):
"""Creates a printable representation of this object, for
debugging purposes"""
return """ z=(%.3f, %.3f) alpha=%.2f beta=%.2f theta=%.2f phi=%.2f
[v=(%.2f, %.2f) u=(%.2f, %.2f) d_ant=%.2f d_post=%.2f xi=%.2f]""" % (self.z.real, self.z.imag, self.alpha, self.beta,
degrees(self.theta), degrees(self.phi),
self.v_left.real, self.v_left.imag, self.u_right.real,
self.u_right.imag, self.d_ant, self.d_post, degrees(self.xi))
class Path():
"""This class implements a path, which is a list of Points"""
p = None # List of points
cyclic = True # Is the path cyclic?
curl_begin = 1 # If not, curl parameter at endpoints
curl_end = 1
def __init__(self, p, tension=1, cyclic=True, curl_begin=1, curl_end=1):
self.p = []
for pt in p:
self.p.append(Point(pt, alpha=1.0/tension, beta=1.0/tension))
self.cyclic = cyclic
self.curl_begin = curl_begin
self.curl_end = curl_end
def range(self):
"""Returns the range of the indexes of the points to be solved.
This range is the whole length of p for cyclic paths, but excludes
the first and last points for non-cyclic paths"""
if self.cyclic:
return range(len(self.p))
else:
return range(1, len(self.p)-1)
# The following functions allow to use a Path object like an array
# so that, if x = Path(...), you can do len(x) and x[i]
def append(self, data):
self.p.append(data)
def __len__(self):
return len(self.p)
def __getitem__(self, i):
"""Gets the point [i] of the list, but assuming the list is
circular and thus allowing for indexes greater than the list
length"""
i %= len(self.p)
return self.p[i]
# Stringfication
def __str__(self):
"""The printable representation of the object is one suitable for
feeding it into tikz, producing the same figure than in metapost"""
r = []
L = len(self.p)
last = 1
if self.cyclic:
last = 0
for k in range(L-last):
post = (k+1)%L
z = self.p[k].z
u = self.p[k].u_right
v = self.p[post].v_left
r.append("(%.4f, %.4f) .. controls (%.5f, %.5f) and (%.5f, %.5f)" % (z.real, z.imag, u.real, u.imag, v.real, v.imag))
if self.cyclic:
last_z = self.p[0].z
else:
last_z = self.p[-1].z
r.append("(%.4f, %.4f)" % (last_z.real, last_z.imag))
return "..".join(r)
def __repr__(self):
"""Dumps internal parameters, for debugging purposes"""
r = ["Path information"]
r.append("Cyclic=%s, curl_begin=%s, curl_end=%s" % (self.cyclic,
self.curl_begin, self.curl_end))
for pt in self.p:
r.append(str(pt))
return "\n".join(r)
# Now some functions from John Hobby and METAFONT book.
# "Velocity" function
def f(theta, phi):
n = 2+sqrt(2)*(sin(theta)-sin(phi)/16)*(sin(phi)-sin(theta)/16)*(cos(theta)-cos(phi))
m = 3*(1 + 0.5*(sqrt(5)-1)*cos(theta) + 0.5*(3-sqrt(5))*cos(phi))
return n/m
def control_points(z0, z1, theta=0, phi=0, alpha=1, beta=1):
"""Given two points in a path, and the angles of departure and arrival
at each one, this function finds the appropiate control points of the
Bezier's curve, using John Hobby's algorithm"""
i = complex(0,1)
u = z0 + cmath.exp(i*theta)*(z1-z0)*f(theta, phi)*alpha
v = z1 - cmath.exp(-i*phi)*(z1-z0)*f(phi, theta)*beta
return(u,v)
def pre_compute_distances_and_angles(path):
"""This function traverses the path and computes the distance between
adjacent points, and the turning angles of the polyline which joins
them"""
for i in range(len(path)):
v_post = path[i+1].z - path[i].z
v_ant = path[i].z - path[i-1].z
# Store the computed values in the Points of the Path
path[i].d_ant = abs(v_ant)
path[i].d_post = abs(v_post)
path[i].xi = arg(v_post/v_ant)
if not path.cyclic:
# First and last xi are zero
path[0].xi = path[-1].xi = 0
# Also distance to previous and next points are zero for endpoints
path[0].d_ant = 0
path[-1].d_post = 0
def build_coefficients(path):
"""This function creates five vectors which are coefficients of a
linear system which allows finding the right values of "theta" at
each point of the path (being "theta" the angle of departure of the
path at each point). The theory is from METAFONT book."""
A=[]; B=[]; C=[]; D=[]; R=[]
pre_compute_distances_and_angles(path)
if not path.cyclic:
# In this case, first equation doesnt follow the general rule
A.append(0)
B.append(0)
curl = path.curl_begin
alpha_0 = path[0].alpha
beta_1 = path[1].beta
xi_0 = (alpha_0**2) * curl / (beta_1**2)
xi_1 = path[1].xi
C.append(xi_0*alpha_0 + 3 - beta_1)
D.append((3 - alpha_0)*xi_0 + beta_1)
R.append(-D[0]*xi_1)
# Equations 1 to n-1 (or 0 to n for cyclic paths)
for k in path.range():
A.append( path[k-1].alpha / ((path[k].beta**2) * path[k].d_ant))
B.append((3-path[k-1].alpha) / ((path[k].beta**2) * path[k].d_ant))
C.append((3-path[k+1].beta) / ((path[k].alpha**2) * path[k].d_post))
D.append( path[k+1].beta / ((path[k].alpha**2) * path[k].d_post))
R.append(-B[k] * path[k].xi - D[k] * path[k+1].xi)
if not path.cyclic:
# The last equation doesnt follow the general form
n = len(R) # index to generate
C.append(0)
D.append(0)
curl = path.curl_end
beta_n = path[n].beta
alpha_n_1 = path[n-1].alpha
xi_n = (beta_n**2) * curl / (alpha_n_1**2)
A.append((3-beta_n)*xi_n + alpha_n_1)
B.append(beta_n*xi_n + 3 - alpha_n_1)
R.append(0)
return (A, B, C, D, R)
import numpy as np # Required to solve the linear equation system
def solve_for_thetas(A, B, C, D, R):
"""This function receives the five vectors created by
build_coefficients() and uses them to build a linear system with N
unknonws (being N the number of points in the path). Solving the system
finds the value for theta (departure angle) at each point"""
L=len(R)
a = np.zeros((L, L))
for k in range(L):
prev = (k-1)%L
post = (k+1)%L
a[k][prev] = A[k]
a[k][k] = B[k]+C[k]
a[k][post] = D[k]
b = np.array(R)
return np.linalg.solve(a,b)
def solve_angles(path):
"""This function receives a path in which each point is "open", i.e. it
does not specify any direction of departure or arrival at each node,
and finds these directions in such a way which minimizes "mock
curvature". The theory is from METAFONT book."""
# Basically it solves
# a linear system which finds all departure angles (theta), and from
# these and the turning angles at each point, the arrival angles (phi)
# can be obtained, since theta + phi + xi = 0 at each knot"""
x = solve_for_thetas(*build_coefficients(path))
L = len(path)
for k in range(L):
path[k].theta = x[k]
for k in range(L):
path[k].phi = - path[k].theta - path[k].xi
def find_controls(path):
"""This function receives a path in which, for each point, the values
of theta and phi (leave and enter directions) are known, either because
they were previously stored in the structure, or because it was
computed by function solve_angles(). From this path description
this function computes the control points for each knot and stores
it in the path. After this, it is possible to print path to get
a string suitable to be feed to tikz."""
r = []
for k in range(len(path)):
z0 = path[k].z
z1 = path[k+1].z
theta = path[k].theta
phi = path[k+1].phi
alpha = path[k].alpha
beta = path[k+1].beta
u,v=control_points(z0, z1, theta, phi, alpha, beta)
path[k].u_right = u
path[k+1].v_left = v
def mp_to_tikz(path, command=None, options=None):
"""Utility funcion which receives a string containing a metapost path
and uses all the above to generate the tikz version with explicit
control points.
It does not make a full parsing of the metapost path. Currently it is
not possible to specify directions nor tensions at knots. It uses
default tension = 1, default curl =1 for both ends in non-cyclic paths
and computes the optimal angles at each knot. It does admit however
cyclic and non-cyclic paths.
To summarize, the only allowed syntax is z0 .. z1 .. z2, where z0, z1,
etc are explicit coordinates such as (0,0) .. (1,0) etc.. And
optionally the path can ends with the literal "cycle"."""
tension = 1
curl = 1
if options:
opt = []
for o in options.split(","):
o=o.strip()
if o.startswith("tension"):
tension = float(o.split("=")[1])
elif o.startswith("curl"):
curl = float(o.split("=")[1])
else:
opt.append(o)
options = ",".join(opt)
new_path = mp_parse(path, default_tension = tension, default_curl = curl)
# print repr(new_path)
solve_angles(new_path)
find_controls(new_path)
if command==None:
command="draw"
if options==None:
options = ""
else:
options = "[%s]" % options
return "\\%s%s %s;" % (command, options, str(new_path))
def mp_parse(mppath, default_tension = 1, default_curl = 1):
"""This function receives a string which contains a path in metapost syntax,
and returns a Path object which stores the same path in the structure
required to compute the control points.
The path should only contain explicit coordinates and numbers.
Currently only "curl" and "tension" keywords are understood. Direction
options are ignored."""
if mppath.endswith(";"): # Remove last semicolon
mppath=mppath[:-1]
pts = mppath.split("..") # obtain points
pts = [p.strip() for p in pts] # remove extra spaces
if pts[-1] == "cycle":
is_cyclic = True
pts=pts[:-1] # Remove this last keyword
else:
is_cyclic = False
path = Path([], cyclic=is_cyclic)
path.curl_begin = default_curl
path.curl_end = default_curl
alpha = beta = 1.0/default_tension
k=0
for p in pts:
if p.startswith("tension"):
aux = p.split()
alpha = 1.0/float(aux[1])
if len(aux)>3:
beta = 1.0/float(aux[3])
else:
beta = alpha
else:
aux = p.split("{") # Extra options at the point
p = aux[0].strip()
if p.startswith("curl"):
if k==0:
path.curl_begin=float(aux[1])
else:
path.curl_end = float(aux[1])
elif p.startswith("dir"):
# Ignored by now
pass
path.append(Point(eval(p))) # store the pair of coordinates
# Update tensions
path[k-1].alpha = alpha
path[k].beta = beta
alpha = beta = 1.0/default_tension
k = k + 1
if is_cyclic:
path[k-1].alpha = alpha
path[k].beta = beta
return path
def main():
"""Example of conversion. Takes a string from stdin and outputs the
result in stdout.
"""
import sys
if len(sys.argv)>2:
opts = sys.argv[2]
else:
opts = None
path = sys.argv[1]
print mp_to_tikz(path, options = opts)
if __name__ == "__main__":
main()
Update
The code supports now tension at each segment, or as a global option for the path. Also changed the way of calling it from latex, using Martin's technique.
Best Answer
Update: This is now available on CTAN (see above). Bugfixes and improvements will generally be available on github before being uploaded to CTAN. Note that that repository is quite cluttered, the relevant
.dtx
file is calledspath3.dtx
. This generates all the necessary files.Update: The code has undergone considerable revision following extensive discussion with Jamie in the chat room From Answers to Packages. The code can now be downloaded from the TeX-SX Launchpad Project. The necessary files are
knots.dtx
andspath.dtx
. The fileknots_test.tex
contains some example code. To create the.sty
files from the.dtx
, runtex <file>.dtx
. Alternatively, running latex (or pdflatex) onknots_test.tex
with shell escapes enabled will automatically generate the.sty
files.Okay, here's my first attempt at implementing what we discussed in chat. It is certainly a bit rough around the edges, and I give absolutely no guarantees that it won't TeX your cat instead.
Code:
Result (draft version on top):
Hopefully the syntax is obvious enough from the example. (It looks a lot nicer in the original PDF; the conversion to PNG is awful!). There are lots of places for improvement, but I thought I'd see if this was on the right track before polishing it.