¿Cuál es la mejor herramienta para dibujar imágenes vectoriales? Para mí y probablemente para muchos otros, la respuesta es bastante obvia: Illustrator o, tal vez, Inkscape. Al menos eso es lo que pensé cuando me pidieron dibujar unos ochocientos diagramas para un libro de texto de física. Nada excepcional, solo un montón de ilustraciones en blanco y negro con esferas, resortes, poleas, lentes, etc. En ese momento ya se sabía que el libro se iba a hacer en LaTeX y me dieron varios documentos de MS Word con imágenes incrustadas. Algunos de ellos eran imágenes escaneadas de otros libros, algunos eran dibujos a lápiz. Imaginar días y noches de tintas de este material me hizo sentir mareado, así que pronto me encontré fantaseando con una solución más automatizada. Por alguna razón,
MetaPost se convirtió en el foco de estas fantasías.

La principal ventaja de usar la solución MetaPost (o similar) es que cada imagen puede ser una especie de función de varias variables. Dicha imagen se puede ajustar rápidamente para cualquier circunstancia imprevista del diseño, sin interrumpir las relaciones internas importantes de la ilustración (algo que realmente me preocupaba), que no se logra fácilmente con herramientas más tradicionales. Además, los elementos recurrentes, todas estas esferas y resortes, pueden hacerse más interesantes visualmente de lo que las herramientas convencionales permitirían con las mismas limitaciones de tiempo.
Quería hacer fotos con algún tipo de eclosión, no muy diferente de lo que encuentras en los libros antiguos.

Primero, necesitaba poder producir algunas curvas de grosor variable. La principal complicación aquí es construir una curva que siga la curva original a una distancia variable. Probablemente utilicé el
método de trabajo más primitivo, que se reduce a simplemente cambiar los segmentos de línea que conectan los puntos de control de la curva de Bezier en una distancia dada, excepto que esta distancia varía a lo largo de la curva.

En la mayoría de los casos, funcionó bien.

Código de ejemploDe aquí en adelante se supone que la biblioteca se
descarga y se
input fiziko.mp;
está presente en el código MetaPost. El método más rápido es usar ConTeXt (entonces no necesita
beginfig
y
endfig
):
\starttext
\startMPcode
input fiziko.mp;
% the code goes here
\stopMPcode
\stoptext
o LuaLaTeX:
\documentclass{article}
\usepackage{luamplib}
\begin{document}
\begin{mplibcode}
input fiziko.mp;
% the code goes here
\end{mplibcode}
\end{document}
beginfig(3);
path p, q; % MetaPost's syntax is reasonably readable, so I'll comment mostly on my stuff
p := (0,-1/4cm){dir(30)}..(5cm, 0)..{dir(30)}(10cm, 1/4cm);
q := offsetPath(p)(1cm*sin(offsetPathLength*pi)); % first argument is the path itself, second is a function of the position along this path (offsetPathLength changes from 0 to 1), which determines how far the outline is from the original line
draw p;
draw q dashed evenly;
endfig;
Se pueden combinar dos contornos para hacer una línea de contorno para un trazo de espesor variable.

Código de ejemplobeginfig(4);
path p, q[];
p := (0,-1/4cm){dir(30)}..(5cm, 0)..{dir(30)}(10cm, 1/4cm);
q1 := offsetPath(p)(1/2pt*(sin(offsetPathLength*pi)**2)); % outline on one side
q2 := offsetPath(p)(-1/2pt*(sin(offsetPathLength*pi)**2)); % and on the other
fill q1--reverse(q2)--cycle;
endfig;
El grosor de la línea debe tener un límite inferior, de lo contrario, algunas líneas serán demasiado delgadas para imprimirse correctamente y esto no se ve muy bien. Una de las opciones (la que elegí) es hacer que las líneas sean muy finas, de modo que la cantidad total de tinta por unidad de longitud permanezca aproximadamente igual que en la línea más delgada deseada. En otras palabras, en lugar de reducir la cantidad de tinta en los lados de la línea, el algoritmo toma algo de tinta de la línea misma.

Código de ejemplobeginfig(5);
path p;
p := (0,-1/4cm){dir(30)}..(5cm, 0)..{dir(30)}(10cm, 1/4cm);
draw brush(p)(1pt*(sin(offsetPathLength*pi)**2)); % the arguments are the same as for the outline
endfig;
Una vez que haya trabajado líneas de grosor variable, puede dibujar esferas. Una esfera se puede representar como una serie de círculos concéntricos con grosores de línea que varían según la salida de una función que calcula la luminosidad de un punto particular en la esfera.

Código de ejemplobeginfig(6);
draw sphere.c(1.2cm);
draw sphere.c(2.4cm) shifted (2cm, 0);
endfig;
Otro bloque de construcción conveniente es un "tubo". En términos generales, es un cilindro que puedes doblar. Mientras el diámetro sea constante, es bastante sencillo.

Código de ejemplobeginfig(7);
path p;
p := subpath (1,8) of fullcircle scaled 3cm;
draw tube.l(p)(1/2cm); % arguments are the path itself and the tube radius
endfig;
Si el diámetro no es constante, las cosas se vuelven más complicadas: la cantidad de golpes debería cambiar de acuerdo con el grosor del tubo para mantener constante la cantidad de tinta por unidad de área antes de tener en cuenta las luces.

Código de ejemplobeginfig(8);
path p;
p := pathSubdivide(fullcircle, 2) scaled 3cm; % this thing splits every segment between the points of a path (here—fullcircle) into several parts (here—2)
draw tube.l(p)(1/2cm + 1/6cm*sin(offsetPathLength*10pi));
endfig;
También hay tubos con trama transversal. El problema de mantener constante la cantidad de tinta resultó ser aún más complicado en este caso, por lo que a menudo estos tubos se ven un poco peludos.

Código de ejemplobeginfig(9);
path p;
p := pathSubdivide(fullcircle, 2) scaled 3cm;
draw tube.t(p)(1/2cm + 1/6cm*sin(offsetPathLength*10pi));
endfig;
Los tubos se pueden utilizar para construir una amplia gama de objetos: desde conos y cilindros hasta balaustres.

Código de ejemplobeginfig(10);
draw tube.l ((0, 0) -- (0, 3cm))((1-offsetPathLength)*1cm) shifted (-3cm, 0); % a very simple cone
path p;
p := (-1/2cm, 0) {dir(175)} .. {dir(5)} (-1/2cm, 1/8cm) {dir(120)} .. (-2/5cm, 1/3cm) .. (-1/2cm, 3/4cm) {dir(90)} .. {dir(90)}(-1/4cm, 9/4cm){dir(175)} .. {dir(5)}(-1/4cm, 9/4cm + 1/5cm){dir(90)} .. (-2/5cm, 3cm); % baluster's envelope
p := pathSubdivide(p, 6);
draw p -- reverse(p xscaled -1) -- cycle;
tubeGenerateAlt(p, p xscaled -1, p rotated -90); % a more low-level stuff than tube.t, first two arguments are tube's sides and the third is the envelope. The envelope is basically a flattened out version of the outline, with line length along the x axis and the distance to line at the y. In the case of this baluster, it's simply its side rotated 90 degrees.
endfig;
Algunas construcciones que se pueden hacer a partir de estas primitivas se incluyen en la biblioteca. Por ejemplo, el globo es básicamente una esfera.

Código de ejemplobeginfig(11);
draw globe(1cm, -15, 0) shifted (-6/2cm, 0); % radius, west longitude, north latitude, both decimal
draw globe(3/2cm, -30.280577, 59.939461);
draw globe(4/3cm, -140, -30) shifted (10/3cm, 0);
endfig;
Sin embargo, la eclosión aquí es latitudinal y controlar la densidad de la línea es mucho más difícil que en las esferas regulares con eclosión "concéntrica", por lo que es un tipo diferente de esfera.

Código de ejemplobeginfig(12);
draw sphere.l(2cm, -60); % diameter and latitude
draw sphere.l(3cm, 45) shifted (3cm, 0);
endfig;
Un peso es una construcción simple hecha de tubos de dos tipos.

Código de ejemplobeginfig(13);
draw weight.s(1cm); % weight height
draw weight.s(2cm) shifted (2cm, 0);
endfig;
También hay una herramienta para anudar los tubos.

Código de ejemplo Por brevedad, solo un nudo.beginfig(14);
path p;
p := (dir(90)*4/3cm) {dir(0)} .. tension 3/2 ..(dir(90 + 120)*4/3cm){dir(90 + 30)} .. tension 3/2 ..(dir(90 - 120)*4/3cm){dir(-90 - 30)} .. tension 3/2 .. cycle;
p := p scaled 6/5;
addStrandToKnot (primeOne) (p, 1/4cm, "l", "1, -1, 1"); % first, we add a strand of width 1/4cm going along the path p to the knot named primeOne. its intersections along the path go to layers "1, -1, 1" and the type of tube is going to be "l".
draw knotFromStrands (primeOne); % then the knot is drawn. you can add more than one strand.
endfig;
Los tubos en nudos caen sombras entre sí como deberían. En teoría, esta característica se puede usar en otros contextos, pero como no tenía planes de profundizar en la tercera dimensión, la interfaz de usuario es algo deficiente y las sombras funcionan correctamente solo para algunos objetos.

Código de ejemplobeginfig(15);
path shadowPath[];
boolean shadowsEnabled;
numeric numberOfShadows;
shadowsEnabled := true; % shadows need to be turned on
numberOfShadows := 1; % number of shadows should be specified
shadowPath0 := (-1cm, -2cm) -- (-1cm, 2cm) -- (-1cm +1/6cm, 2cm) -- (-1cm + 1/8cm, -2cm) -- cycle; % shadow-dropping object should be a closed path
shadowDepth0 := 4/3cm; % it's just this high above the object on which the shadow falls
shadowPath1 := shadowPath0 rotated -60;
shadowDepth1 := 4/3cm;
draw sphere.c(2.4cm); % shadows work ok only with sphere.c and tube.l with constant diameter
fill shadowPath0 withcolor white;
draw shadowPath0;
fill shadowPath1 withcolor white;
draw shadowPath1;
endfig;
Ciertamente, necesitará una textura de madera (actualización: desde que se publicó la versión rusa de este artículo, el primer caso de esta biblioteca se usó en un proyecto real que sé que
ha ocurrido , y fue la textura de madera la que vino útil, así que esto no terminó siendo una broma después de todo). Cómo las ramitas y su crecimiento afectan el patrón de los anillos anuales es un tema de estudio serio. El modelo de trabajo más simple que se me ocurre es el siguiente: los anillos anuales son superficies planas paralelas, distorsionadas por ramas en crecimiento; así, la superficie se modifica por una serie de "funciones de ramita" no demasiado complejas en diferentes lugares y las isolinas de la superficie se toman como el patrón de anillo anual.

Código de ejemplobeginfig(16);
numeric w, b;
pair A, B, C, D, A', B', C', D';
w := 4cm;
b := 1/2cm;
A := (0, 0);
A' := (b, b);
B := (0, w);
B' := (b, wb);
C := (w, w);
C' := (wb, wb);
D := (w, 0);
D' := (wb, b);
draw woodenThing(A--A'--B'--B--cycle, 0); % a piece of wood inside the A--A'--B'--B--cycle path, with wood grain at 0 degrees
draw woodenThing(B--B'--C'--C--cycle, 90);
draw woodenThing(C--C'--D'--D--cycle, 0);
draw woodenThing(A--A'--D'--D--cycle, 90);
eyescale := 2/3cm; % scale for the eye
draw eye(150) shifted 1/2[A,C]; % the eye looks in 150 degree direction
endfig;
El ojo de la imagen de arriba se abre de par en par o se entrecierra un poco y su pupila también cambia su tamaño. Puede que no tenga ningún sentido práctico, pero los ojos mecánicamente similares solo parecen aburridos.

Código de ejemplobeginfig(17);
eyescale := 2/3cm; % 1/2cm by default
draw eye(0) shifted (0cm, 0);
draw eye(0) shifted (1cm, 0);
draw eye(0) shifted (2cm, 0);
draw eye(0) shifted (3cm, 0);
draw eye(0) shifted (4cm, 0);
endfig;
La mayoría de las veces las ilustraciones no eran tan complejas, pero un enfoque más riguroso requeriría resolver muchos de los problemas en el libro de texto para ilustrarlos correctamente. Digamos, el problema de la polea de L'Hôpital (no estaba en ese libro de texto, pero de todos modos): en la cuerda con la longitud
suspendido en el punto
cuelga una polea; está enganchado a otra cuerda, suspendido en el punto
con el peso
en su final La pregunta es: ¿a dónde va el peso si tanto la polea como las cuerdas no pesan nada? Sorprendentemente, la solución y la construcción de este problema no son tan simples. Pero al jugar con varias variables, puede hacer que la imagen se vea bien para la página mientras se mantiene la precisión.

Código de ejemplovardef lHopitalPulley (expr AB, l, m) = % distance AB between the suspension points of the ropes and their lengths l and m. “Why no units of length?”, you may ask. It's because some calculations inside can cause arithmetic overflow in MetaPost.
save A, B, C, D, E, o, a, x, y, d, w, h, support;
image(
pair A, B, C, D, E, o[];
path support;
numeric a, x[], y[], d[], w, h;
x1 := (l**2 + abs(l)*((sqrt(8)*AB)++l))/4AB; % the solution
y1 := l+-+x1; % second coordinate is trivial
y2 := m - ((AB-x1)++y1); % as well as the weight's position
A := (0, 0);
B := (AB*cm, 0);
D := (x1*cm, -y1*cm);
C := D shifted (0, -y2*cm);
d1 := 2/3cm; d2 := 1cm; d3 := 5/6d1; % diameters of the pulley, weight and the pulley wheel
w := 2/3cm; h := 1/3cm; % parameters of the wood block
o1 := (unitvector(CD) rotated 90 scaled 1/2d3);
o2 := (unitvector(DB) rotated 90 scaled 1/2d3);
E := whatever [D shifted o1, C shifted o1]
= whatever [D shifted o2, B shifted o2]; % pulley's center
a := angle(AD);
support := A shifted (-w, 0) -- B shifted (w, 0) -- B shifted (w, h) -- A shifted (-w, h) -- cycle;
draw woodenThing(support, 0); % wood block everything is suspended from
draw pulley (d1, a - 90) shifted E; % the pulley
draw image(
draw A -- D -- B withpen thickpen;
draw D -- C withpen thickpen;
) maskedWith (pulleyOutline shifted E); % ropes should be covered with the pulley
draw sphere.c(d2) shifted C shifted (0, -1/2d2); % sphere as a weight
dotlabel.llft(btex $A$ etex, A);
dotlabel.lrt(btex $B$ etex, B);
dotlabel.ulft(btex $C$ etex, C);
label.llft(btex $l$ etex, 1/2[A, D]);
)
enddef;
beginfig(18);
draw lHopitalPulley (6, 2, 11/2); % now you can choose the right parameters
draw lHopitalPulley (3, 5/2, 3) shifted (8cm, 0);
endfig;
¿Y qué hay del libro de texto? Por desgracia, cuando casi todas las ilustraciones y el diseño estaban listos, algo sucedió y el libro de texto fue cancelado. Tal vez por eso, decidí reescribir la mayoría de las funciones de la biblioteca original desde cero (elegí no usar ninguno de los códigos originales, que, aunque indirectamente, me pagaron) y
ponerlo en GitHub . Algunas cosas, presentes en la biblioteca original, como las funciones para dibujar automóviles y tractores, no las incluí allí, se agregaron algunas características nuevas, por ejemplo, nudos.
No se ejecuta rápidamente: toma aproximadamente un minuto producir todas las imágenes para este artículo con LuaLaTeX en mi computadora portátil con i5-4200U 1.6 GHz. Se usa un generador de números pseudoaleatorios aquí y allá, por lo que no hay dos imágenes similares que sean absolutamente idénticas (esa es una característica) y cada ejecución produce imágenes ligeramente diferentes. Para evitar sorpresas, simplemente puede establecer
randomseed := some number
y disfrutar de los mismos resultados en cada ejecución.
Muchas gracias al
Dr. ord y a
Mikael Sundqvist por su ayuda con la versión en inglés de este texto.