Diverses choses dans MetaPost

Quel est le meilleur outil à utiliser pour dessiner des images vectorielles? Pour moi et probablement pour beaucoup d'autres, la réponse est assez évidente: Illustrator, ou, peut-être, Inkscape. C'est du moins ce que je pensais quand on m'a demandé de dessiner environ huit cents diagrammes pour un manuel de physique. Rien d'exceptionnel, juste un tas d'illustrations en noir et blanc avec des sphères, des ressorts, des poulies, des lentilles, etc. À ce moment-là, on savait déjà que le livre allait être fait en LaTeX et on m'a donné un certain nombre de documents MS Word avec des images intégrées. Certains d'entre eux étaient des images numérisées d'autres livres, d'autres des dessins au crayon. En imaginant des jours et des nuits à encrer ce truc, je me suis senti étourdi, alors je me suis vite retrouvé à fantasmer sur une solution plus automatisée. Pour une raison quelconque, MetaPost est devenu le centre de ces fantasmes.





Le principal avantage de l'utilisation de la solution MetaPost (ou similaire) est que chaque image peut être une sorte de fonction de plusieurs variables. Une telle image peut être rapidement ajustée pour toute circonstance imprévue de la mise en page, sans perturber les relations internes importantes de l'illustration (ce qui m'inquiétait vraiment), ce qui n'est pas facile à réaliser avec des outils plus traditionnels. En outre, les éléments récurrents, toutes ces sphères et ressorts, peuvent être rendus plus intéressants visuellement que les outils conventionnels ne le permettraient avec les mêmes contraintes de temps.

Je voulais faire des images avec une sorte de hachure, un peu comme ce que l'on rencontre dans les vieux livres.



Tout d'abord, j'avais besoin de pouvoir produire des courbes d'épaisseur variable. La principale complication ici est de construire une courbe qui suit la courbe d'origine à une distance variable. J'ai probablement utilisé la méthode de travail la plus primitive, qui se résume à simplement déplacer les segments de ligne reliant les points de contrôle de la courbe de Bézier d'une distance donnée, sauf que cette distance varie le long de la courbe.



Dans la plupart des cas, cela a bien fonctionné.



Exemple de code
À partir de là, il est supposé que la bibliothèque est téléchargée et input fiziko.mp; est présent dans le code MetaPost. La méthode la plus rapide consiste à utiliser ConTeXt (alors vous n'avez pas besoin de beginfig et endfig ):

\starttext
\startMPcode
input fiziko.mp;
% the code goes here
\stopMPcode
\stoptext


ou 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;



Deux contours peuvent être combinés pour créer une ligne de contour pour un trait d'épaisseur variable.



Exemple de code
beginfig(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;



L'épaisseur de la ligne devrait avoir une limite inférieure, sinon, certaines lignes vont être trop fines pour être correctement imprimées et cela n'a pas l'air génial. L'une des options (celle que j'ai choisie) est de rendre les lignes trop fines en pointillés, de sorte que la quantité totale d'encre par unité de longueur reste approximativement la même que dans la ligne plus fine prévue. En d'autres termes, au lieu de réduire la quantité d'encre sur les côtés de la ligne, l'algorithme prend de l'encre sur la ligne elle-même.



Exemple de code
beginfig(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;



Une fois que vous avez des lignes d'épaisseur variable, vous pouvez dessiner des sphères. Une sphère peut être représentée comme une série de cercles concentriques avec des épaisseurs de ligne variant en fonction de la sortie d'une fonction qui calcule la légèreté d'un point particulier sur la sphère.



Exemple de code
beginfig(6);
draw sphere.c(1.2cm);
draw sphere.c(2.4cm) shifted (2cm, 0);
endfig;



Un autre bloc de construction pratique est un «tube». En gros, c'est un cylindre que vous pouvez plier. Tant que le diamètre est constant, c'est assez simple.



Exemple de code
beginfig(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 le diamètre n'est pas constant, les choses se compliquent: le nombre de traits doit changer en fonction de l'épaisseur du tube afin de maintenir constante la quantité d'encre par unité de surface avant de prendre en compte les lumières.



Exemple de code
beginfig(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;



Il existe également des tubes à hachures transversales. Le problème de maintenir la quantité d'encre constante s'est avéré encore plus délicat dans ce cas, donc souvent ces tubes ont l'air un peu hirsutes.



Exemple de code
beginfig(9);
path p;
p := pathSubdivide(fullcircle, 2) scaled 3cm;
draw tube.t(p)(1/2cm + 1/6cm*sin(offsetPathLength*10pi));
endfig;



Les tubes peuvent être utilisés pour construire une large gamme d'objets: des cônes et des cylindres aux balustres.



Exemple de code
beginfig(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;



Certaines constructions qui peuvent être faites à partir de ces primitives sont incluses dans la bibliothèque. Par exemple, le globe est essentiellement une sphère.



Exemple de code
beginfig(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;



Cependant, les hachures ici sont latitudinales et le contrôle de la densité des lignes est beaucoup plus difficile que sur les sphères régulières avec des hachures «concentriques», c'est donc un type de sphère différent.



Exemple de code
beginfig(12);
draw sphere.l(2cm, -60); % diameter and latitude
draw sphere.l(3cm, 45) shifted (3cm, 0);
endfig;



Un poids est une construction simple faite de tubes de deux types.



Exemple de code
beginfig(13);
draw weight.s(1cm); % weight height
draw weight.s(2cm) shifted (2cm, 0);
endfig;



Il y a aussi un outil pour nouer les tubes.



Exemple de code. Par souci de concision, un seul nœud.
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;



Les tubes en nœuds se déposent des ombres les uns sur les autres comme ils le devraient. En théorie, cette fonctionnalité peut être utilisée dans d'autres contextes, mais comme je n'avais pas l'intention d'aller en profondeur dans la troisième dimension, l'interface utilisateur manque quelque peu et les ombres ne fonctionnent correctement que pour certains objets.



Exemple de code
beginfig(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;



Certes, vous aurez besoin d'une texture bois (mise à jour: depuis la publication de la version russe de cet article, le premier cas de cette bibliothèque utilisée dans un vrai projet dont je suis au courant s'est produit , et c'est la texture bois qui est venue en pratique, donc cela n'a finalement pas été une blague après tout). Comment les brindilles et leur croissance affectent le modèle des anneaux annuels est un sujet de réflexion sérieux. Le modèle de travail le plus simple que j'ai pu trouver est le suivant: les anneaux annuels sont des surfaces planes parallèles, déformées par des rameaux en croissance; ainsi la surface est modifiée par une série de «fonctions de brindille» pas trop complexes à différents endroits et les isolignes de la surface sont prises comme motif d'anneau annuel.



Exemple de code
beginfig(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;



L'œil de l'image ci-dessus s'ouvre grand ou plisse un peu et sa pupille change également de taille. Cela peut ne pas avoir de sens pratique, mais les yeux similaires sur le plan mécanique semblent ennuyeux.



Exemple de code
beginfig(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 plupart du temps, les illustrations n'étaient pas si complexes, mais une approche plus rigoureuse nécessiterait de résoudre bon nombre des problèmes du manuel pour les illustrer correctement. Disons, le problème de poulie de L'Hôpital (ce n'était pas dans ce manuel, mais de toute façon): sur la corde avec la longueur lsuspendu au point Aune poulie pend; il est accroché à une autre corde, suspendu au point Bavec le poids Cà sa fin. La question est: où va le poids si la poulie et les cordes ne pèsent rien? Étonnamment, la solution et la construction de ce problème ne sont pas si simples. Mais en jouant avec plusieurs variables, vous pouvez faire en sorte que l'image soit parfaitement adaptée à la page tout en conservant une précision.



Exemple de code
vardef 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;



Et qu'en est-il du manuel? Hélas, lorsque presque toutes les illustrations et la mise en page étaient prêtes, quelque chose s'est produit et le manuel a été annulé. Peut-être à cause de cela, j'ai décidé de réécrire la plupart des fonctions de la bibliothèque d'origine à partir de zéro (j'ai choisi de ne pas utiliser le code d'origine, pour lequel, bien qu'indirectement, j'étais payé) et de le mettre sur GitHub . Certaines choses, présentes dans la bibliothèque d'origine, telles que les fonctions de dessin de voitures et de tracteurs, je n'y ai pas inclus, de nouvelles fonctionnalités, par exemple des nœuds, ont été ajoutées.

Il ne s'exécute pas rapidement: il faut environ une minute pour produire toutes les images de cet article avec LuaLaTeX sur mon ordinateur portable avec i5-4200U 1,6 GHz. Un générateur de nombres pseudo-aléatoires est utilisé ici et là, donc aucune image similaire n'est absolument identique (c'est une caractéristique) et chaque exécution produit des images légèrement différentes. Pour éviter les surprises, vous pouvez simplement définir randomseed := some number et profiter des mêmes résultats à chaque exécution.

Un grand merci à dr ord et Mikael Sundqvist pour leur aide avec la version anglaise de ce texte.

Source: https://habr.com/ru/post/fr454376/


All Articles