Skip to content

Commit 90aeb13

Browse files
authored
feat: annotate equations (#5)
1 parent 29fe7c4 commit 90aeb13

File tree

2 files changed

+242
-75
lines changed

2 files changed

+242
-75
lines changed

src/annotate-equations.sty

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
% Annotate LaTeX Equations
2+
%
3+
% (c) 2022 by ST John, https://github.com/st--/
4+
% Licensed under MIT License
5+
%
6+
\NeedsTeXFormat{LaTeX2e}[1994/06/01]
7+
\ProvidesPackage{annotate-equations}
8+
[2022/03/29 v0.1.0 easily annotate equations using TikZ]
9+
10+
%%% lualatex compatibility, from https://tex.stackexchange.com/a/351520/171664
11+
\RequirePackage{ifluatex}
12+
\ifluatex
13+
\RequirePackage{luatex85}
14+
\RequirePackage{pdftexcmds}
15+
\makeatletter
16+
\let\pdfstrcmp\pdf@strcmp
17+
\let\pdffilemoddate\pdf@filemoddate
18+
\makeatother
19+
\fi
20+
%%%
21+
22+
\RequirePackage{tikz}
23+
\RequirePackage{xcolor}
24+
25+
\usetikzlibrary{backgrounds}
26+
\usetikzlibrary{arrows,shapes}
27+
\usetikzlibrary{tikzmark} % for \tikzmarknode
28+
\usetikzlibrary{calc} % for computing the midpoint between two nodes, e.g. at ($(p1.north)!0.5!(p2.north)$)
29+
30+
31+
%%%%% SETTINGS %%%%%
32+
33+
\newcommand{\eqnhighlightheight}{} % colorbox will shrink to content
34+
\renewcommand{\eqnhighlightheight}{\mathstrut} % colorbox will always have full height
35+
36+
\newcommand{\eqnhighlightshade}{17} % light
37+
%\renewcommand{\eqnhighlightshade}{47} % dark
38+
39+
\newcommand{\eqnannotationtext}[1]{\sffamily\footnotesize#1\strut} %%% TODO remove textsf/footnotesize/strut?
40+
41+
42+
\providecommand\EAmarkanchor{north} % default set to "above"
43+
\providecommand\EAwesteast{east} % default set to "right"
44+
\providecommand\EAlabelanchor{south} % default set to "label above"
45+
% for pgfkeys, see https://tex.stackexchange.com/a/34318/171664
46+
% for no-value keys, see https://tex.stackexchange.com/a/401848/171664
47+
\pgfkeys{
48+
/eqnannotate/.is family, /eqnannotate,
49+
above/.code = {\renewcommand\EAmarkanchor{north}},
50+
below/.code = {\renewcommand\EAmarkanchor{south}},
51+
left/.code = {\renewcommand\EAwesteast{west}},
52+
right/.code = {\renewcommand\EAwesteast{east}},
53+
label above/.code = {\renewcommand\EAlabelanchor{south}},
54+
label below/.code = {\renewcommand\EAlabelanchor{north}},
55+
}
56+
57+
%%%%% %%%%%%%% %%%%%
58+
59+
60+
\newcommand*{\eqnhighlightcolorbox}[2]{%
61+
% \colorbox sets the second argument in text mode, so for use within equations we wrap it in $ $ again
62+
\mathchoice% to get right font size in each mode:
63+
{\colorbox{#1}{$\displaystyle #2$}}%
64+
{\colorbox{#1}{$\textstyle #2$}}%
65+
{\colorbox{#1}{$\scriptstyle #2$}}%
66+
{\colorbox{#1}{$\scriptscriptstyle #2$}}%
67+
}
68+
69+
%%% the fbox with 0pt rule fixes the height of eqnmark vs eqnmarkbox issue
70+
\newcommand*{\eqnhighlightfbox}[2]{%
71+
% \fbox sets the second argument in text mode, so for use within equations we wrap it in $ $ again
72+
\mathchoice% to get right font size in each mode:
73+
{\begingroup\setlength{\fboxrule}{0pt}\fbox{$\displaystyle\color{#1}#2$}\endgroup}%
74+
{\begingroup\setlength{\fboxrule}{0pt}\fbox{$\textstyle\color{#1}#2$}\endgroup}%
75+
{\begingroup\setlength{\fboxrule}{0pt}\fbox{$\scriptstyle\color{#1}#2$}\endgroup}%
76+
{\begingroup\setlength{\fboxrule}{0pt}\fbox{$\scriptscriptstyle\color{#1}#2$}\endgroup}%
77+
}
78+
79+
% . is the current color
80+
81+
\newcommand*{\eqnhighlight}[2]{\begingroup\colorlet{currentcolor}{.}\eqnhighlightcolorbox{#1!\eqnhighlightshade}{\eqnhighlightheight #2}\endgroup}
82+
\newcommand*{\eqncolor}[2]{\begingroup\colorlet{currentcolor}{.}\eqnhighlightfbox{#1}{\eqnhighlightheight #2}\endgroup}
83+
84+
%%% Arguments to \eqnmark[box]: [highlight color]{node name}{term to highlight}
85+
\newcommand*{\eqnmarkbox}[3][currentcolor]{\addvalue{#2}{#1}\tikzmarknode{#2}{\eqnhighlight{#1}{#3}}}
86+
\newcommand*{\eqnmark}[3][currentcolor]{\addvalue{#2}{#1}\tikzmarknode{#2}{\eqncolor{#1}{#3}}}
87+
88+
% Store current color in a dictionary (lookup table),
89+
% from https://tex.stackexchange.com/a/48931/171664 :
90+
\def\addvalue#1#2{\expandafter\gdef\csname eqnannotate@data@#1\endcsname{#2}}
91+
\def\usevalue#1{%
92+
\ifcsname eqnannotate@data@#1\endcsname
93+
\csname eqnannotate@data@#1\expandafter\endcsname
94+
\else
95+
currentcolor%
96+
\fi
97+
}
98+
99+
100+
%%%%% Helpers for swapping north/south / west/east / -/+ depending on above/below / left/right etc.:
101+
\newcommand*{\swapNorthSouth}[1]{%
102+
\ifnum\pdfstrcmp{#1}{south}=0 north\else south\fi
103+
}
104+
\newcommand*{\swapWestEast}[1]{%
105+
\ifnum\pdfstrcmp{#1}{east}=0 west\else east\fi
106+
}
107+
\newcommand*{\EAxshift}[1]{%
108+
\ifnum\pdfstrcmp{#1}{east}=0 -0.3ex\else 0.3ex\fi
109+
}
110+
%%%%%
111+
112+
113+
\newcounter{eqnannotatenode}
114+
\newcommand*{\eqnannotateCurrentNode}{eqnannotatenode\theeqnannotatenode}
115+
116+
117+
\newcommand{\annotatetwo}[5][]{%
118+
\begingroup% %%% so we don't leak the \def's below
119+
\stepcounter{eqnannotatenode}%
120+
%%% #1: (optional) extra args for \node e.g. yshift=...
121+
\pgfkeys{/eqnannotate, #2}% %%% all configuration options
122+
\def\myEAmarkOne{#3}%
123+
\def\myEAmarkTwo{#4}%
124+
\colorlet{currentcolor}{.}
125+
\def\myEAtext{#5}%
126+
\def\myEAcolor{\usevalue{\myEAmarkOne}}%
127+
\begin{tikzpicture}[overlay,remember picture,>=stealth,nodes={align=left,inner ysep=1pt},<-]
128+
% default anchor is at center
129+
\node[anchor=\swapNorthSouth{\EAmarkanchor},color=\myEAcolor!85,#1] % color blended with white to 85%, any (optional) extra args #1
130+
(\eqnannotateCurrentNode) % use counter-based "local node"
131+
at ($(\myEAmarkOne.\EAmarkanchor)!0.5!(\myEAmarkTwo.\EAmarkanchor)$) % centered between the two nodes
132+
{\eqnannotationtext{\myEAtext}};
133+
% double arrow to two uses within the equation:
134+
\draw [<->,color=\myEAcolor] (\myEAmarkOne.\EAmarkanchor) |- ([yshift=0.1ex] \eqnannotateCurrentNode.\EAlabelanchor) -| (\myEAmarkTwo.\EAmarkanchor); % from node 1 via annotation to node 2, with anchor #6 each
135+
\end{tikzpicture}%
136+
\endgroup% %%% close group again
137+
}
138+
139+
140+
%%% \extractfirst from https://tex.stackexchange.com/a/115733/171664
141+
\RequirePackage{expl3}
142+
\RequirePackage{xparse}
143+
\ExplSyntaxOn
144+
\NewDocumentCommand{\extractfirst}{mm}
145+
{
146+
\tl_set:Nx #1 {\clist_item:Nn #2 { 1 } }
147+
}
148+
\ExplSyntaxOff
149+
%%%
150+
151+
\newcommand{\annotate}[4][]{%
152+
\begingroup% %%% so we don't leak the \def's below
153+
\stepcounter{eqnannotatenode}%
154+
%
155+
%
156+
%
157+
% #1: (optional) extra args for \node e.g. yshift=...
158+
\pgfkeys{/eqnannotate, #2}
159+
\def\myEAmarks{#3}%
160+
\extractfirst\myEAmark\myEAmarks% %%% get first node for color and annotation
161+
\def\myEAtext{#4}%
162+
%
163+
%
164+
\colorlet{currentcolor}{.}
165+
\def\myEAcolor{\usevalue{\myEAmark}}%
166+
%
167+
%
168+
\def\myEAxshift{\EAxshift{\EAwesteast}}%
169+
\begin{tikzpicture}[overlay,remember picture,>=stealth,nodes={align=left,inner ysep=1pt},<-]
170+
\node[anchor=\swapNorthSouth{\EAmarkanchor} \swapWestEast{\EAwesteast},color=\myEAcolor!85,#1] % TODO for some reason, passing #1 through command doesn't work...
171+
% anchor=west: align left edge of text on top of tikzmark in equation
172+
% should be north west for below and south west for above ...
173+
(\eqnannotateCurrentNode) at (\myEAmark.\EAmarkanchor) % \EAmarkanchor north: above the equation, south: below
174+
{\eqnannotationtext{\myEAtext}};
175+
\foreach \EAmark in \myEAmarks
176+
\draw [color=\myEAcolor] (\EAmark.\EAmarkanchor) % arrow from the equation
177+
% \EAmarkanchor north: above the equation, south: below
178+
|- ([xshift=\myEAxshift,yshift=0.1ex] \eqnannotateCurrentNode.south \EAwesteast);
179+
% - south east: we want line to end at bottom right of annotation text;
180+
% - negative xshift makes it a little bit shorter;
181+
% - yshift for aesthetics (\strut is ever so slightly too tall).
182+
\end{tikzpicture}%
183+
\endgroup% %%% close group again
184+
}
185+
186+
\endinput
187+
%%
188+
%% End of file

0 commit comments

Comments
 (0)