26

I am wondering if there exists a way to simulate paintbrush strokes in TikZ when filling in a shape. So given the following:

\documentclass{article}
\usepackage{tikz}
\begin{document}
\begin{tikzpicture}
\tikz{
\draw[fill=red](0,0)rectangle(10,10);
}
\end{tikzpicture}
\end{document}

Instead of a solid red shape I was hoping to make it look like the red part was painted on with a paintbrush. I'm not exactly sure how what that would look like but I'm guessing maybe some randomly wavy lines that are a little darker and some maybe that are a little lighter? And I guess one would want for the paintbrush to have a thickness so that you'd be able to see the difference between one stroke and the one next to it.

Looking at examples of actual paintbrush strokes online it looks like the light/dark parts vary a little on each line but that might be a complication not worth pursuing.

Having all the strokes go down would be fine but being able to indicate a directions would be cool.

And while I'm sure something like this is possible in various image editing programs, I need to do this in TeX/LaTeX as part of an automated file generating process.

Edit: I do not believe this is a duplicate of the chalkboard solution as that one appears to use a bunch of small dots whereas a paintbrush involves long wavy lines of varying shades of the root color. The results would look very different. It could be that the chalkboard question provides an idea for an approach but using it as-is would not be a solution to my question.

Update 1: I have done some experimentation with using TikZ decorations with wavy, random lines with rounded corners and it seems like this could be a way forward but I haven't made anything that looks close to convincing.

Update 2: As per below, here is a link to a picture of close-up of brush strokes. This is pretty exaggerated but gets the point across. Here is an attempt at using wavy random lines:

\documentclass{article}
\usepackage{tikz}
\usetikzlibrary{decorations.pathmorphing}
\begin{document}
\begin{tikzpicture}
\tikz[line width=.1mm]{
\draw[fill=red](0,0)rectangle(10,10);
\draw[line width = .5mm,decorate,decoration={random steps,segment length=12pt,amplitude=1pt},rounded corners=.1pt, color=red!50!brown] (1.5,0) -- (1.5,10);
\draw[line width = .5mm,decorate,decoration={random steps,segment length=12pt,amplitude=1pt},rounded corners=.1pt, color=red!50!brown] (1.65,0) -- (1.65,10);
\draw[line width = .5mm,decorate,decoration={random steps,segment length=12pt,amplitude=1pt},rounded corners=.1pt, color=red!50!brown] (1.8,0) -- (1.8,10);
\draw[line width = .5mm,decorate,decoration={random steps,segment length=12pt,amplitude=1pt},rounded corners=.1pt, color=red!50!brown] (1.95,0) -- (1.95,10);
}
\end{tikzpicture}
\end{document}

As you can see it looks terrible.

Red with brush strokes attempt

Update 3: Let me give a bit more detail about what I'm looking for. I am looking for something to fill large geometric shapes like squares, rectangles, and circles with sizes from half a page to almost a full page with one color. I have a program that generates music in many different styles (I'm a composer) but have decided to add artwork to it as well. So far I'm sticking with 20th century Modernist stuff as a lot of it seems simpler to do. I did a Mondrian one already (the squares and rectangles on thick crossing lines). And what inspired this question was the works of Kazimir Malevich, specifically his Black Square, Black Cross, Red Square and a few others of a similar style. My software randomly generates "paintings" that look similar but not exactly the same (for example, the black square varies in size but is still large, the red square uses different random dimensions for the quadrilateral, the black circle has different random dimensions and placed at random, etc). I had thought that since these are so simple that my users would appreciate having it be a bit more interesting to look at with the simulated brush strokes. Unfortunately I don't have any specific examples of paintings/painters in mind, just some vague notion that brush marks could be seen on paintings like those if you look closely enough(though I have no idea if you can see the brush strokes in Malevich's paintings). But then making it too subtle might get lost on the user when looking at it on their computer or phone so being a bit exaggerated might be better? It also occurs to me that maybe the bumps of the canvas underneath might help with the illusion?

  • 1
    I guess you could build on the answers to tex.stackexchange.com/q/334341/121799. – marmot Feb 16 at 3:30
  • I hadn't seen that. It does seem relevant. – bfootdav Feb 16 at 3:39
  • @Raaja from what I can tell the chalkboard solution creations thousands of dots whereas a paintbrush would create long wavy lines of various shades of the root color. There might be something helpful in that solution but it doesn't get the look I'm going for. – bfootdav Feb 16 at 6:53
  • Perhaps you can simply throw us some famous piece or names. For instance Mark Rothko. And people will start analyzing how to achieve that. – Symbol 1 Feb 18 at 3:01
  • @Symbol1 I added Update 3 to my post to respond to your comment here. Unfortunately I don't have specific painters in mind but I did provide links to the paintings I am simulating with my software. I only have a vague notion of what brush strokes might look like on those paintings but I have been to enough museums to know that if you look closely enough you can see them. I'm definitely not going for Van Gogh-type strokes but wouldn't say no to that either. Rothko's are bit more ragged around the edges than I'm looking for but now I'm wondering if the look of the canvas underneath might help? – bfootdav Feb 18 at 4:27
23

This is a quickly written proposal based on this answer, which makes use of this answer. I plan to improve it later. (I am really not sure if understand the efforts to close the question. The chalk board post is IMHO related, but this question is IMHO not a duplicate thereof.)

\documentclass[tikz,border=3.14mm]{standalone}
\usetikzlibrary{decorations,arrows.meta,bending}
\begin{document}
\pgfdeclarearrow{
  name=ink,
  parameters= {\the\pgfarrowlength},  
  setup code={
   \pgfarrowssettipend{0pt}
   \pgfarrowssetlineend{-\pgfarrowlength}
   \pgfarrowlinewidth=\pgflinewidth
   \pgfarrowssavethe\pgfarrowlength
  },
  drawing code={
   \pgfpathmoveto{\pgfpoint{-\pgfarrowlength}{0.5\pgflinewidth}}
   \pgfpathcurveto{\pgfpoint{-0.75\pgfarrowlength}{0.6\pgflinewidth}}{%
   \pgfpoint{-0.01\pgfarrowlength}{0.6\pgflinewidth}}{%
   \pgfpoint{0pt}{0pt}}
   \pgfpathcurveto{\pgfpoint{-0.01\pgfarrowlength}{-0.5\pgflinewidth}}{%
   \pgfpoint{-0.2\pgfarrowlength}{-(1+0.3*rnd)*\pgflinewidth}}{%
   \pgfpoint{-0.3\pgfarrowlength}{-0.8*(1+0.3*rnd)*\pgflinewidth}}
   \pgfpathcurveto{\pgfpoint{-0.4\pgfarrowlength}{-0.6*(1+0.3*rnd)*\pgflinewidth}}{%
   \pgfpoint{-0.6\pgfarrowlength}{-0.3*(1+0.3*rnd)*\pgflinewidth}}{%
   \pgfpoint{-1\pgfarrowlength}{-0.5\pgflinewidth}}
   \pgfusepathqfill
  },
  defaults = { length = 12pt }
}
\pgfkeys{/pgf/decoration/.cd,
         start color/.store in=\startcolor,
         start color=black,
         end color/.store in=\endcolor,
         end color=black,
         varying line width steps/.initial=100
}
\pgfdeclaredecoration{width and color change}{initial}{
 \state{initial}[width=0pt, next state=line, persistent precomputation={%
   \pgfmathparse{\pgfdecoratedpathlength/\pgfkeysvalueof{/pgf/decoration/varying line width steps}}%
   \let\increment=\pgfmathresult%
   \def\x{0}%
 }]{}
 \state{line}[width=\increment pt,   persistent postcomputation={%
   \pgfmathsetmacro{\x}{\x+\increment}
   },next state=line]{%
   \pgfmathparse{ifthenelse(\x<\pgfdecoratedpathlength-5mm,varyinglw(100*(\x/\pgfdecoratedpathlength)),
    varyinglw(100*((\pgfdecoratedpathlength-5mm)/\pgfdecoratedpathlength))*(\pgfdecoratedpathlength-\x)/14) )}
   \pgfmathparse{varyinglw(100*(\x/\pgfdecoratedpathlength))} %<-changed
   \pgfsetlinewidth{\pgfmathresult pt}%
   \pgfpathmoveto{\pgfpointorigin}%
   \pgfmathsetmacro{\steplength}{1.4*\increment}
   \pgfpathlineto{\pgfqpoint{\steplength pt}{0pt}}%
   \pgfmathsetmacro{\y}{100*(\x/\pgfdecoratedpathlength)}
   \pgfsetstrokecolor{\endcolor!\y!\startcolor}%
   \pgfusepath{stroke}%
 }
 \state{final}{%
   \pgfsetlinewidth{\pgflinewidth}%
   \pgfpathmoveto{\pgfpointorigin}%
   \pgfmathsetmacro{\y}{100*(\x/\pgfdecoratedpathlength)}
   \color{\endcolor!\y!\startcolor}%
   \pgfusepath{stroke}% 
 }
}


\begin{tikzpicture}[varying arrow/.style={-{ink[length=5mm,width=3.2mm]},color=\endcolor,
postaction={/utils/exec=\pgfsetarrows{-},decorate,decoration={width and color change}}
}]
\begin{scope}[declare function={varyinglw(\x)=12+5*rnd;}]
\foreach \X in {0,...,5}
{\draw[%/pgf/decoration/start color=red,/pgf/decoration/end color=red,
decorate,decoration={width and color change,start color=red,end color=red}] 
(0,-\X*0.7-0.1+0.2*rnd) to[bend left=10-20*rnd] ++ (3,0);}
\end{scope}

\end{tikzpicture}
\end{document} 

enter image description here

UPDATE: Something that goes a little bit into the direction of the picture under the link that you added in your update. Takes rather long to compile and is far from satisfying. I am posting this merely as a report on where I went, hoping that others may find some of this useful.

\documentclass[tikz,border=3.14mm]{standalone}
\usetikzlibrary{decorations,decorations.pathreplacing,calc,positioning}
\begin{document}

\pgfkeys{/brush pars/.cd,
lines/.initial=16,
color/.code={\colorlet{brushcolor}{#1}},
color=red,
distance/.initial=0.4pt
}
\tikzset{
    brush/.style={
        decorate,
        decoration={
            show path construction,
            lineto code={
                \foreach\Xbrush in{1,...,\pgfkeysvalueof{/brush pars/lines}}{
                 \pgfmathrandomitem{\c}{color}
                 \pgfmathtruncatemacro{\mix}{100-24*rnd}
                 \draw[color=brushcolor!\mix!\c,
                 shorten >={(3-4*rnd)*1pt
                    -0.5*\pgfkeysvalueof{/brush pars/lines}*\pgfkeysvalueof{/brush pars/distance}},
                 shorten <={(3-4*rnd)*1pt
                 -0.5*\pgfkeysvalueof{/brush pars/lines}*\pgfkeysvalueof{/brush pars/distance}}]
                 let \p1=($(\tikzinputsegmentlast)-(\tikzinputsegmentfirst)$),
                 \n1={90+atan2(\y1,\x1)} in
                 ($(\tikzinputsegmentfirst)+(\n1:{((1.02-0.04*rnd)*\Xbrush-\pgfkeysvalueof{/brush pars/lines}/2)*\pgfkeysvalueof{/brush pars/distance}})$)
                 --
                ($(\tikzinputsegmentlast)+(\n1:{((1.02-0.04*rnd)*\Xbrush-\pgfkeysvalueof{/brush pars/lines}/2)*\pgfkeysvalueof{/brush pars/distance}})$);
                }
            },
            curveto code={
                \foreach\Xbrush in{1,...,\pgfkeysvalueof{/brush pars/lines}}{
                 \pgfmathrandomitem{\c}{color}
                 \pgfmathtruncatemacro{\mix}{100-24*rnd}
                 \draw[color=brushcolor!\mix!\c,shorten >={(3-4*rnd)*1pt
                    -0.5*\pgfkeysvalueof{/brush pars/lines}*\pgfkeysvalueof{/brush pars/distance}},
                 shorten <={(3-4*rnd)*1pt
                    -0.5*\pgfkeysvalueof{/brush pars/lines}*\pgfkeysvalueof{/brush pars/distance}}]
                 let \p1=($(\tikzinputsegmentsupporta)-(\tikzinputsegmentfirst)$),
                 \p2=($(\tikzinputsegmentsupportb)-(\tikzinputsegmentsupporta)$),
                 \p3=($(\tikzinputsegmentlast)-(\tikzinputsegmentsupportb)$),
                 \n1={90+atan2(\y1,\x1)}, \n2={90+atan2(\y2,\x2)}, 
                 \n3={90+atan2(\y3,\x3)} in
                 ($(\tikzinputsegmentfirst)+(\n1:{((1.02-0.04*rnd)*\Xbrush-\pgfkeysvalueof{/brush pars/lines}/2)*\pgfkeysvalueof{/brush pars/distance}})$)
                 .. controls ($(\tikzinputsegmentsupporta)+(\n2:{((1.02-0.04*rnd)*\Xbrush-\pgfkeysvalueof{/brush pars/lines}/2)*\pgfkeysvalueof{/brush pars/distance}})$)
                  and ($(\tikzinputsegmentsupportb)+(\n3:{((1.02-0.04*rnd)*\Xbrush-\pgfkeysvalueof{/brush pars/lines}/2)*\pgfkeysvalueof{/brush pars/distance}})$) ..
                ($(\tikzinputsegmentlast)+(\n3:{((1.02-0.04*rnd)*\Xbrush-\pgfkeysvalueof{/brush pars/lines}/2)*\pgfkeysvalueof{/brush pars/distance}})$);
                }
            },
        }
    }
}
\tikzset{pics/.cd,
  A/.style={code={\draw[brush] 
  (0,-0.55) -- (0.3,0.4) -- (0.6,-0.55);
  \draw[brush](0.1,1/3-0.45) --
  (0.5,1/3-0.45);
  \path (0.7,0);}},
  B/.style={code={\draw[brush] (0,-0.45) -- (0,0.45)
  to[out=0,in=0,looseness=2.5]  (0,0)  to[out=0,in=0,looseness=3] cycle;}},
  C/.style={code={\draw[brush]
   (0,0) to[out=90,in=110,looseness=2]  (0.5,0.25);
   \draw[brush](0,0) to[out=-90,in=-110,looseness=2]  (0.5,-0.25);
   \path (0.7,0);}},
  D/.style={code={\draw[brush] (0,-0.45) -- (0,0.45) 
  to[out=0,in=0,looseness=2.25]   cycle;
  \path (0.7,0);}},
  E/.style={code={\draw[brush] 
  (0.5,-0.45) --(0,-0.45) -- (0,0.45)  -- (0.5,0.45);
  \draw[brush] (0,0) -- (0.5,0);
  \path (0.7,0);}},
  F/.style={code={\draw[brush] 
  (0,-0.45) -- (0,0.45)  -- (0.5,0.45);
  \draw[brush] (0,0) -- (0.5,0);
  \path (0.7,0);}},
  G/.style={code={\draw[brush]
   (0,0) to[out=90,in=110,looseness=2]  (0.5,0.25);
   \draw[brush] (0,0) to[out=-90,in=-110,looseness=2]  
   (0.5,-0.25);
   \draw[brush] (0.54,-0.25) to (0.3,-0.25);
   \path (0.7,0);}},
  H/.style={code={\draw[brush] 
  (0,-0.5) -- (0,0.5);  
  \draw[brush] (0.5,-0.5) -- (0.5,0.5);
  \draw[brush] (0,0) -- (0.5,0);
  \path (0.7,0);}},
  I/.style={code={\draw[brush] (0,-0.45) -- (0,0.45);
  \path (0.25,0);}},
  J/.style={code={\draw[brush] (0.2,0.45) -- (0.2,-0.35) to[out=-90,in=0]
  (0.1,-0.45) to[out=180,in=-90] (0,-0.35);
  \path (0.45,0);}},
  K/.style={code={\draw[brush] 
  (0,-0.45) -- (0,0.45); 
  \draw[brush] (0.4,0.45) -- (0.02,0) --  (0.4,-0.45);
  \path (0.6,0);}},
  L/.style={code={\draw[brush] 
  (0,0.5) -- (0,-0.45) -- (0.4,-0.45);
  \path (0.6,0);}},
  M/.style={code={\draw[brush] (0,-0.45) -- (0,0.45) -- 
  (0.3,0.25) -- (0.6,0.45) -- (0.6,-0.45);
  \path (0.8,0);}},
  N/.style={code={\draw[brush] (0,-0.45) -- (0,0.45) -- (0.6,-0.4) --
  (0.6,0.45);
  \path (0.8,0);}},
  O/.style={code={\draw[brush] (0.3,0) circle(0.3 and 0.48);
  \path (0.8,0);}},
  P/.style={code={\draw[brush] (0,-0.45) -- (0,0.45) 
  to[out=0,in=0,looseness=2.5]  (0,0);
  \path (0.6,0);}},
  Q/.style={code={\draw[brush] 
  (0.3,0) circle(0.3 and 0.48);
  \draw[brush](0.35,-0.25) -- (0.6,-0.45);
  \path (0.8,0);}},
  R/.style={code={\draw[brush] 
  (0,-0.45) -- (0,0.45) 
  to[out=0,in=0,looseness=2.5]  (0.05,0) -- (0.4,-0.45);
  \path (0.6,0);}},
  S/.style={code={\draw[brush] (0.5,0.4)  
  to[out=160,in=165,looseness=2]  (0.3,0)  
  to[out=-15,in=-20,looseness=2] (0.1,-0.4);
  \path (0.65,0);}},
  T/.style={code={\draw[brush] (0.35,-0.45) -- (0.35,0.45) (0,0.45) -- (0.7,0.45);
  \path (0.85,0);}},
  U/.style={code={\draw[brush] (0,0.5) -- (0,0) to[out=-90,in=-90,looseness=2.5]
  (0.6,0) -- (0.6,0.5);
  \path (0.8,0);}},
  V/.style={code={\draw[brush] (0,0.5) -- (0.3,-0.4) -- (0.6,0.5);
  \path (0.8,0);}},
  W/.style={code={\draw[brush] (0,0.45) -- (0.3,-0.4) -- (0.45,-0.1)
  -- (0.6,-0.4) -- (0.9,0.45);
  \path (1.1,0);}},
  X/.style={code={\draw[brush] 
  (0,0.45) -- (0.6,-0.45);
   \draw[brush] (0.6,0.45)
  -- (0,-0.45);
  \path (0.8,0);}},
  Y/.style={code={\draw[brush] 
  (0,0.45) -- (0.3,0);  
  \draw[brush] (0.6,0.45)
  -- (0,-0.45);
  \path (0.8,0);}},
  Z/.style={code={\draw[brush] (0,0.45) --(0.6,0.45) -- (0,-0.45)  
  -- (0.6,-0.45);
  \path (0.8,0);}},
  space/.style={code={\path (0,0) (0.2,0);}},
}
\pgfmathdeclarerandomlist{color}{{black}{white}}
\begin{tikzpicture}
 \pic[local bounding box=box1,scale=2] at (0,0) {A};
 \foreach \X [count=\Y,evaluate=\Y as \Z using {int(\Y+1)}] in {B,...,Z}
  {\edef\temp{\noexpand\pic[right=0mm of box\Y,local bounding box=box\Z,scale=2]
  {\X};}
  \temp}
\end{tikzpicture}
\end{document}

enter image description here

enter image description here

Time needed to compile the full alphabet on my machine:

real    0m11.438s
user    0m10.758s
sys 0m0.622s

The letters are taken from this answer and really very quickly written. (They were meant to go into the Christmas extravaganza but didn't make it there for good reasons.)

And within less than 4 minutes (on my machine) you get

\documentclass[tikz,border=3.14mm]{standalone}
\usetikzlibrary{decorations,decorations.pathreplacing,calc,positioning}
\pgfkeys{/brush pars/.cd,
lines/.initial=16,
color/.code={\colorlet{brushcolor}{#1}},
color=red,
distance/.initial=0.4pt
}
\tikzset{
    brush/.style={
        decorate,
        decoration={
            show path construction,
            lineto code={
                \foreach\Xbrush in{1,...,\pgfkeysvalueof{/brush pars/lines}}{
                 \pgfmathrandomitem{\c}{color}
                 \pgfmathtruncatemacro{\mix}{100-24*rnd}
                 \draw[color=brushcolor!\mix!\c,
                 shorten >={(3-4*rnd)*1pt
                    -0.5*\pgfkeysvalueof{/brush pars/lines}*\pgfkeysvalueof{/brush pars/distance}},
                 shorten <={(3-4*rnd)*1pt
                 -0.5*\pgfkeysvalueof{/brush pars/lines}*\pgfkeysvalueof{/brush pars/distance}}]
                 let \p1=($(\tikzinputsegmentlast)-(\tikzinputsegmentfirst)$),
                 \n1={90+atan2(\y1,\x1)} in
                 ($(\tikzinputsegmentfirst)+(\n1:{((1.02-0.04*rnd)*\Xbrush-\pgfkeysvalueof{/brush pars/lines}/2)*\pgfkeysvalueof{/brush pars/distance}})$)
                 --
                ($(\tikzinputsegmentlast)+(\n1:{((1.02-0.04*rnd)*\Xbrush-\pgfkeysvalueof{/brush pars/lines}/2)*\pgfkeysvalueof{/brush pars/distance}})$);
                }
            },
            curveto code={
                \foreach\Xbrush in{1,...,\pgfkeysvalueof{/brush pars/lines}}{
                 \pgfmathrandomitem{\c}{color}
                 \pgfmathtruncatemacro{\mix}{100-24*rnd}
                 \draw[color=brushcolor!\mix!\c,shorten >={(3-4*rnd)*1pt
                    -0.5*\pgfkeysvalueof{/brush pars/lines}*\pgfkeysvalueof{/brush pars/distance}},
                 shorten <={(3-4*rnd)*1pt
                    -0.5*\pgfkeysvalueof{/brush pars/lines}*\pgfkeysvalueof{/brush pars/distance}}]
                 let \p1=($(\tikzinputsegmentsupporta)-(\tikzinputsegmentfirst)$),
                 \p2=($(\tikzinputsegmentsupportb)-(\tikzinputsegmentsupporta)$),
                 \p3=($(\tikzinputsegmentlast)-(\tikzinputsegmentsupportb)$),
                 \n1={90+atan2(\y1,\x1)}, \n2={90+atan2(\y2,\x2)}, 
                 \n3={90+atan2(\y3,\x3)} in
                 ($(\tikzinputsegmentfirst)+(\n1:{((1.02-0.04*rnd)*\Xbrush-\pgfkeysvalueof{/brush pars/lines}/2)*\pgfkeysvalueof{/brush pars/distance}})$)
                 .. controls ($(\tikzinputsegmentsupporta)+(\n2:{((1.02-0.04*rnd)*\Xbrush-\pgfkeysvalueof{/brush pars/lines}/2)*\pgfkeysvalueof{/brush pars/distance}})$)
                  and ($(\tikzinputsegmentsupportb)+(\n3:{((1.02-0.04*rnd)*\Xbrush-\pgfkeysvalueof{/brush pars/lines}/2)*\pgfkeysvalueof{/brush pars/distance}})$) ..
                ($(\tikzinputsegmentlast)+(\n3:{((1.02-0.04*rnd)*\Xbrush-\pgfkeysvalueof{/brush pars/lines}/2)*\pgfkeysvalueof{/brush pars/distance}})$);
                }
            },
        }
    }
}
\pgfmathdeclarerandomlist{color}{{black}{white}}
\begin{document}
\begin{tikzpicture}
\draw[clip,postaction={fill=red}] (0,0) rectangle (10,10);
\foreach \X in {1,...,1000}
{\draw[brush] (-0.5+11*rnd,-0.5+11*rnd) to[bend left={30-60*rnd}]
++ (360*rnd:1+2*rnd);}

\end{tikzpicture}
\end{document}

enter image description here

And for more aligned strokes you may try

\documentclass[tikz,border=3.14mm]{standalone}
\usetikzlibrary{decorations,decorations.pathreplacing,calc,positioning}
\pgfkeys{/brush pars/.cd,
lines/.initial=16,
color/.code={\colorlet{brushcolor}{#1}},
color=red,
distance/.initial=0.4pt
}
\tikzset{
    brush/.style={
        decorate,
        decoration={
            show path construction,
            lineto code={
                \foreach\Xbrush in{1,...,\pgfkeysvalueof{/brush pars/lines}}{
                 \pgfmathrandomitem{\c}{color}
                 \pgfmathtruncatemacro{\mix}{100-24*rnd}
                 \draw[color=brushcolor!\mix!\c,
                 shorten >={(3-4*rnd)*1pt
                    -0.5*\pgfkeysvalueof{/brush pars/lines}*\pgfkeysvalueof{/brush pars/distance}},
                 shorten <={(3-4*rnd)*1pt
                 -0.5*\pgfkeysvalueof{/brush pars/lines}*\pgfkeysvalueof{/brush pars/distance}}]
                 let \p1=($(\tikzinputsegmentlast)-(\tikzinputsegmentfirst)$),
                 \n1={90+atan2(\y1,\x1)} in
                 ($(\tikzinputsegmentfirst)+(\n1:{((1.02-0.04*rnd)*\Xbrush-\pgfkeysvalueof{/brush pars/lines}/2)*\pgfkeysvalueof{/brush pars/distance}})$)
                 --
                ($(\tikzinputsegmentlast)+(\n1:{((1.02-0.04*rnd)*\Xbrush-\pgfkeysvalueof{/brush pars/lines}/2)*\pgfkeysvalueof{/brush pars/distance}})$);
                }
            },
            curveto code={
                \foreach\Xbrush in{1,...,\pgfkeysvalueof{/brush pars/lines}}{
                 \pgfmathrandomitem{\c}{color}
                 \pgfmathtruncatemacro{\mix}{100-24*rnd}
                 \draw[color=brushcolor!\mix!\c,shorten >={(3-4*rnd)*1pt
                    -0.5*\pgfkeysvalueof{/brush pars/lines}*\pgfkeysvalueof{/brush pars/distance}},
                 shorten <={(3-4*rnd)*1pt
                    -0.5*\pgfkeysvalueof{/brush pars/lines}*\pgfkeysvalueof{/brush pars/distance}}]
                 let \p1=($(\tikzinputsegmentsupporta)-(\tikzinputsegmentfirst)$),
                 \p2=($(\tikzinputsegmentsupportb)-(\tikzinputsegmentsupporta)$),
                 \p3=($(\tikzinputsegmentlast)-(\tikzinputsegmentsupportb)$),
                 \n1={90+atan2(\y1,\x1)}, \n2={90+atan2(\y2,\x2)}, 
                 \n3={90+atan2(\y3,\x3)} in
                 ($(\tikzinputsegmentfirst)+(\n1:{((1.02-0.04*rnd)*\Xbrush-\pgfkeysvalueof{/brush pars/lines}/2)*\pgfkeysvalueof{/brush pars/distance}})$)
                 .. controls ($(\tikzinputsegmentsupporta)+(\n2:{((1.02-0.04*rnd)*\Xbrush-\pgfkeysvalueof{/brush pars/lines}/2)*\pgfkeysvalueof{/brush pars/distance}})$)
                  and ($(\tikzinputsegmentsupportb)+(\n3:{((1.02-0.04*rnd)*\Xbrush-\pgfkeysvalueof{/brush pars/lines}/2)*\pgfkeysvalueof{/brush pars/distance}})$) ..
                ($(\tikzinputsegmentlast)+(\n3:{((1.02-0.04*rnd)*\Xbrush-\pgfkeysvalueof{/brush pars/lines}/2)*\pgfkeysvalueof{/brush pars/distance}})$);
                }
            },
        }
    }
}
\pgfmathdeclarerandomlist{color}{{black}{white}}
\begin{document}
\begin{tikzpicture}[declare function={VF(\x,\y)=10*\x-4*\y+2*\x*\y;}]
\draw[clip,postaction={fill=red}] (0,0) rectangle (10,10);
\foreach \X in {1,...,1000}
{\pgfmathsetmacro{\myx}{-0.5+11*rnd}
\pgfmathsetmacro{\myy}{-0.5+11*rnd}
\draw[brush] (\myx,\myy) to[bend left={30-60*rnd}]
++ ({VF(\myx,\myy)+10-20*rnd}:1+2*rnd);}
\end{tikzpicture}
\end{document}

enter image description here

  • I like the random nature on the outside of the boxes but it doesn't really have the long lines that you see in paintings where you can see the brush strokes. It's that look that I'm going for giving it texture. I've played around with TikZ decorations using random wavy lines with rounded corners but the results so far have been terrible. – bfootdav Feb 16 at 19:05
  • @bfootdav I guess that the main problem which prevents many to write an answer is that it not clear what the target is. If you google "paint brush", you get many ideas. (The main reason that I wrote the answer was to prevent your question, which I like, from being closed.) So to help others to answer the question, please add a picture of the result you are after, and also some of your attempts. – marmot Feb 16 at 19:09
  • Ok, I've added an example, my attempt, and an image of that result. – bfootdav Feb 16 at 19:36
  • that's really nice, thank you! I like how strong the textured look is. I didn't realize how long render times were going to be for this. My use case is large simple geometric shapes (like the square in my original post, circles, etc) that are filled in with one color and take up a quarter to half a page in size. I think, based on your and the other answer, I'm not going to be able to include this functionality given the time it will take to fill a 10x10 rectangle . Having said that, I have been unable to get yours to draw the straight lines I would need to fill a rectangle. – bfootdav Feb 17 at 18:40
  • Yep, that does it. It takes about a minute and a half on my ageing laptop to render a 2x2 square. I am going to have to think long and hard about this. But in the meantime thank you very much, it really does look nice when rendered. And even using solid black looks good. – bfootdav Feb 17 at 19:12
31

This one takes 6 seconds to compile. It is based on my previous answer to the chalkboard texture. The idea is to use dash pattern to reduce the for loop. (Graphic card does the job instead of TeX.)

\documentclass[border=9,tikz]{standalone}
\usetikzlibrary{decorations.pathreplacing}
\begin{document}

\def\niterate{128}
\def\rolldice{
    \pgfmathsetmacro\a{(1+rnd)/10}
    \pgfmathsetmacro\b{5+5*rnd}
    \pgfmathsetmacro\c{1+rnd}
    \pgfmathsetmacro\d{rnd*3}
    \pgfmathsetmacro\dark{rnd*50+50}
}
\tikzset{
    put dots/.style={
        /utils/exec=\rolldice,
        line width=\a,
        dash pattern=on \b off \c,
        dash phase=\c*rnd,
        shift={(rnd*360:\d pt)},
        line cap=round,
        black!\dark,
        opacity=.8
    },
    chalk/.style={
        decorate,
        decoration={
            show path construction,
            lineto code={
                \foreach\i in{1,...,\niterate}{
                    \draw[put dots]
                        (\tikzinputsegmentfirst)--(\tikzinputsegmentlast);
                }
            },
            curveto code={
                \foreach\i in{1,...,\niterate}{
                    \draw[put dots]
                        (\tikzinputsegmentfirst)..controls
                        (\tikzinputsegmentsupporta)and(\tikzinputsegmentsupportb)
                        ..(\tikzinputsegmentlast);
                }
            },
            closepath code={
                \foreach\i in{1,...,\niterate}{
                \draw[put dots]
                    (\tikzinputsegmentfirst)--(\tikzinputsegmentlast);
                }
            }
        }
    }
}

\tikz[looseness=0.25]{
    \path [chalk] (1/8,2) -- (0,1/2) arc (180:315:1/2) (-1/2,3/2)
        to [bend right] (5/8,3/2);
    \path [chalk, shift=(0:1)] (1/8,1) to [bend left] (0,0);
    \path [chalk, shift=(0:3/2)] (1/8,2) to [bend left] (0,0)
         (2/3,1) -- (1/16,2/3) -- (2/3,0);
    \path [chalk, shift=(0:5/2)] (0,1) to [bend left] (1,1)
         to [bend left] (0,0) to [bend left] (1,0);
    \path [chalk] (-1,-3/4) to [bend left] (9/2,-1/2);
}


\message{^^J^^J time = \the\dimexpr\pdfelapsedtime sp (pt means second) ^^J^^J}

\end{document}


Some Optimization (not necessary better)

  • Start with thick curves with less shifting and more stable color.
    This fills the canvas more efficiently.
  • as \i goes up, put thinner and thinner curves with more randomness.
    This creates the brush texture on the top of the base color.
  • Use opacity < 1 so that overlapping curves look like more curves.
  • The following code uses only 50 bezier curves to replace one bezier curves.
    It takes 3 seconds to compile.

\documentclass[border=9,tikz]{standalone}
\usetikzlibrary{decorations.pathreplacing}
\begin{document}

\def\niterate{50}
\def\rolldice{
    \pgfmathsetmacro\rndlinewidth{6/(2+\i)}
    \pgfmathsetmacro\rndon{8+8*rnd}
    \pgfmathsetmacro\rndoff{2*rnd}
    \pgfmathsetmacro\rndshift{sqrt((1-\rndlinewidth/2)*5*rnd)}
    \pgfmathsetmacro\rndblend{50+\i*rand}
}
\tikzset{
    put dashes/.style={
        /utils/exec=\rolldice,
        line width=\rndlinewidth,
        dash pattern=on \rndon off \rndoff,
        dash phase=(\rndon+\rndoff)*rnd,
        shift={(rnd*360:\rndshift pt)},
        line cap=round,
        blue!\rndblend!green,
        opacity=.6
    },
    chalk/.style={
        decorate,
        decoration={
            show path construction,
            lineto code={
                \foreach\i in{1,...,\niterate}{
                    \draw[put dashes]
                        (\tikzinputsegmentfirst)--(\tikzinputsegmentlast);
                }
            },
            curveto code={
                \foreach\i in{1,...,\niterate}{
                    \draw[put dashes]
                        (\tikzinputsegmentfirst)..controls
                        (\tikzinputsegmentsupporta)and(\tikzinputsegmentsupportb)
                        ..(\tikzinputsegmentlast);
                }
            },
            closepath code={
                \foreach\i in{1,...,\niterate}{
                \draw[put dashes]
                    (\tikzinputsegmentfirst)--(\tikzinputsegmentlast);
                }
            }
        }
    }
}

\tikz[looseness=0.25]{
    \path [chalk] (1/8,2) -- (0,1/2) arc (180:315:1/2) (-1/2,3/2)
        to [bend right] (5/8,3/2);
    \path [chalk, shift=(0:1)] (1/8,1) to [bend left] (0,0);
    \path [chalk, shift=(0:3/2)] (1/8,2) to [bend left] (0,0)
         (2/3,1) -- (1/16,2/3) -- (2/3,0);
    \path [chalk, shift=(0:5/2)] (0,1) to [bend left] (1,1)
         to [bend left] (0,0) to [bend left] (1,0);
    \path [chalk] (-1,-3/4) to [bend left] (9/2,-1/2);
}


\message{^^J^^J time = \the\numexpr\pdfelapsedtime*1000/65536 ms ^^J^^J}

\end{document}

  • 1
    Nice! That's what we needed: a simple effect that convincingly creates the illusion of brush strokes. – Circumscribe Feb 17 at 21:56
  • I confirm the 6 seconds. Really nice!!!! – marmot Feb 17 at 22:02
  • @Symbol1 This is terrific and fast! The part I'm struggling with is something I didn't make clear in my original post, that I'm trying to use this to fill large simple geometric shapes in with so that they look painted. Like a 10cm x 10cm square. Setting aside the issue of how much time it takes, when yours, and the others, are used to fill a large square it loses a lot of its randomness and looks more like a cloth or wooden texture, I'm not sure where to go from there. Regardless, this looks amazing and works really well, thanks! – bfootdav Feb 18 at 1:51
  • @bfootdav Does this make more sense? gist.github.com/Symbol1/6e9c164612770eb9875e1dc01b7af88a – Symbol 1 Feb 18 at 2:39
  • Nice 4730 ms on my computer! Can you add the link to your previous answer of the chalkboard texture? – AndréC Feb 18 at 4:54
25

New version

I've had some time to think about a to accomplish what I was trying to do more elegantly/efficiently, and to look at how the show path construction decoration (which is used in both Symbol1's answer and Marmot's answer, but which I had never used before) works. I think I've come up with a better implementation that should be a lot faster/less memory-hoggy. (It's more than 10× faster than the old version.)

I'm still drawing brush strokes by drawing a lot of thinner strokes of different colours with non-constant thickness. Instead of drawing these by concatenating short line fragments, however, I'm drawing one curve that follows the desired path precisely and another one that sort of orbits around it. To do this I've written some code that splits up the Bézier curve into smaller pieces of (very) roughly equal length with some random offset.

Disclaimer: the code below is currently still buggy, unoptimised and uncustomisable. I will further improve the answer somewhere in the not too distant future. The image below was produced in around 11 seconds on my machine.

\documentclass[tikz,margin=3.14pt]{standalone}
\usetikzlibrary{decorations.pathreplacing}

\tikzset{brush hair offset/.initial=0pt}

\makeatletter
%% For legibility:
\def\br@bezFrstAx{\dimen0}
\def\br@bezFrstBx{\dimen2}
\def\br@bezFrstCx{\dimen4}
\def\br@bezScndAx{\dimen6}
\def\br@bezScndBx{\dimen8}
\def\br@bezThrdx{\dimen10}
\def\br@bezFrstAy{\dimen12}
\def\br@bezFrstBy{\dimen14}
\def\br@bezFrstCy{\dimen16}
\def\br@bezScndAy{\dimen18}
\def\br@bezScndBy{\dimen20}
\def\br@bezThrdy{\dimen22}
\newif\iffirstcomponent

%% Construct control points for a pair of
\newcommand*\splitbezier[5]{\begingroup\edef\x{\endgroup\noexpand\splitbezier@{#1}#2#3#4#5\noexpand\splitbezier@}\x}
\def\splitbezier@#1(#2,#3)(#4,#5)(#6,#7)(#8,#9)\splitbezier@{%
  \begingroup
    \pgfmathsetmacro\s{#1}%
    \ifdim\s pt>17pt \def\s{17}\fi
    \ifdim\s pt<-16pt \def\s{-16}\fi
    \pgfmathsubtract@{1}{\s}\let\t\pgfmathresult
    %% Linear curves:
    \br@bezFrstAx=\dimexpr\t\dimexpr#2\relax+\s\dimexpr#4\relax
    \br@bezFrstAy=\dimexpr\t\dimexpr#3\relax+\s\dimexpr#5\relax
    \br@bezFrstBx=\dimexpr\t\dimexpr#4\relax+\s\dimexpr#6\relax
    \br@bezFrstBy=\dimexpr\t\dimexpr#5\relax+\s\dimexpr#7\relax
    \br@bezFrstCx=\dimexpr\t\dimexpr#6\relax+\s\dimexpr#8\relax
    \br@bezFrstCy=\dimexpr\t\dimexpr#7\relax+\s\dimexpr#9\relax
    %% Quadratic curves:
    \br@bezScndAx=\dimexpr\t\br@bezFrstAx+\s\br@bezFrstBx
    \br@bezScndAy=\dimexpr\t\br@bezFrstAy+\s\br@bezFrstBy
    \br@bezScndBx=\dimexpr\t\br@bezFrstBx+\s\br@bezFrstCx
    \br@bezScndBy=\dimexpr\t\br@bezFrstBy+\s\br@bezFrstCy
    %% Cubic curve:
    \br@bezThrdx= \dimexpr\t\br@bezScndAx+\s\br@bezScndBx
    \br@bezThrdy =\dimexpr\t\br@bezScndAy+\s\br@bezScndBy
    \edef\x{\endgroup %% <- Smuggling operation in progress
      \def\noexpand\bezOneStart{#2,#3}%
      \def\noexpand\bezOneControlA{\the\br@bezFrstAx,\the\br@bezFrstAy}%
      \def\noexpand\bezOneControlB{\the\br@bezScndAx,\the\br@bezScndAy}%
      \def\noexpand\bezOneEnd{\the\br@bezThrdx,\the\br@bezThrdy}%
      \def\noexpand\bezTwoStart{\the\br@bezThrdx,\the\br@bezThrdy}%
      \def\noexpand\bezTwoControlA{\the\br@bezScndBx,\the\br@bezScndBy}%
      \def\noexpand\bezTwoControlB{\the\br@bezFrstCx,\the\br@bezFrstCy}%
      \def\noexpand\bezTwoEnd{#8,#9}%
    }\x
}
\newcommand*\splitstraighttwice[4]{\begingroup\edef\x{\endgroup\noexpand\splitstraight@{#1}#2#3\noexpand#4\noexpand\splitstraight@}\x}
\def\splitstraight@#1(#2,#3)(#4,#5)#6\splitstraight@{%
  \begingroup
    \pgfmathsetmacro\t{#1}%
    \pgfpointlineattime{\t}{\pgfpoint{#2}{#3}}{\pgfpoint{#4}{#5}}%
    \edef#6{\the\pgf@x,\the\pgf@y}%
    \pgfmath@smuggleone#6%
   \endgroup
}
\newcommand*\shiftbezier[6]{%
  \pgfmathsetmacro\shiftbezier@tmpA{#1}%
  \pgfmathsetmacro\shiftbezier@tmpB{#2}%
  \begingroup\edef\x{\endgroup\noexpand\shiftbezier@#3#4#5#6\noexpand\shiftbezier@}\x
}
\def\shiftbezier@(#1,#2)(#3,#4)(#5,#6)(#7,#8)\shiftbezier@{% %% <- pity this can't have 10 arguments...
    \pgfpointscale{\shiftbezier@tmpA}{\pgfpointnormalised{\pgfpoint{\dimexpr#3-#1}{\dimexpr#4-#2}}}%
    \edef\bezOneStart{\the\dimexpr#1-\pgf@y-0\pgf@x,\the\dimexpr#2+\pgf@x-0\pgf@y}%
    \edef\bezOneControlA{\the\dimexpr#3-\pgf@y,\the\dimexpr#4+\pgf@x}%
    \pgfpointscale{\shiftbezier@tmpB}{\pgfpointnormalised{\pgfpoint{\dimexpr#7-#5}{\dimexpr#8-#6}}}%
    \edef\bezOneControlB{\the\dimexpr#5-\pgf@y,\the\dimexpr#6+\pgf@x}%
    \edef\bezOneEnd{\the\dimexpr#7-\pgf@y+0\pgf@x,\the\dimexpr#8+\pgf@x+0\pgf@y}%
}
% \pgftransformlineattime{#1}{\pgfpoint{#2}{#3}}{\pgfpoint{#4}{#5}}
\tikzset{
  brush hair/.default=orange,
  brush hair/.style={ %% <- subdivide path
  decorate,
  /utils/exec={\pgfmathsetmacro\storedsegmoffset{rnd}\pgfmathsetmacro\storedsegmamp{(2*random(2)-3)*0.2pt}},
    decoration={
      show path construction,
      lineto code={ %% Using a Bezier curve for this is very inefficient, but I'm lazy. I will improve this later.
        \splitstraighttwice{0.333333}{(\tikzinputsegmentfirst)}{(\tikzinputsegmentlast)}\firstintpoint
        \splitstraighttwice{0.666667}{(\tikzinputsegmentfirst)}{(\tikzinputsegmentlast)}\secondintpoint
        \draw[brush hair=#1] (\tikzinputsegmentfirst)..controls(\firstintpoint)and(\secondintpoint)..(\tikzinputsegmentlast);
      },
      curveto code={
        \color{#1}
        \pgfmathsetmacro\hairoffset{\pgfkeysvalueof{/tikz/brush hair offset}}
        \pgfmathsetmacro\segmcount{\pgfdecoratedinputsegmentlength/5mm}
        \let\segmoffset\storedsegmoffset
        \let\segmamp\storedsegmamp
        \pgfmathparse{-\segmoffset/\segmcount}
        \splitbezier{\pgfmathresult}{(\tikzinputsegmentfirst)}{(\tikzinputsegmentsupporta)}
                                    {(\tikzinputsegmentsupportb)}{(\tikzinputsegmentlast)}
        \pgfmathsetmacro\segmcount{\segmcount+\segmoffset}
        \loop\ifdim\segmcount pt>1pt
          \pgfmathparse{1/\segmcount}
          \splitbezier{\pgfmathresult}{(\bezTwoStart)}{(\bezTwoControlA)}{(\bezTwoControlB)}{(\bezTwoEnd)}
          \ifdim\segmoffset pt=0pt
            \begingroup
              \shiftbezier{\hairoffset}{\hairoffset}{(\bezOneStart)}{(\bezOneControlA)}{(\bezOneControlB)}{(\bezOneEnd)}
              \draw[-,line cap=round] (\bezOneStart)..controls(\bezOneControlA)and(\bezOneControlB)..(\bezOneEnd);
              \shiftbezier{\segmamp}{-\segmamp}{(\bezOneStart)}{(\bezOneControlA)}{(\bezOneControlB)}{(\bezOneEnd)}
              \draw[-,line cap=round] (\bezOneStart)..controls(\bezOneControlA)and(\bezOneControlB)..(\bezOneEnd);
            \endgroup
          \else
            \ifdim\pgfdecoratedcompleteddistance=0pt
              \def\extralength{.1*rnd}
            \else
              \def\extralength{0}
            \fi
            \shiftbezier{\hairoffset}{\hairoffset}{(\bezOneStart)}{(\bezOneControlA)}{(\bezOneControlB)}{(\bezOneEnd)}
            \begingroup
              \splitbezier{\segmoffset-\extralength}{(\bezOneStart)}{(\bezOneControlA)}{(\bezOneControlB)}{(\bezOneEnd)}
              \draw[-,line cap=round] (\bezTwoStart)..controls(\bezTwoControlA)and(\bezTwoControlB)..(\bezTwoEnd);
            \endgroup
            \begingroup
              \shiftbezier{\segmamp}{-\segmamp}{(\bezOneStart)}{(\bezOneControlA)}{(\bezOneControlB)}{(\bezOneEnd)}
              \splitbezier{\segmoffset-\extralength}{(\bezOneStart)}{(\bezOneControlA)}{(\bezOneControlB)}{(\bezOneEnd)}
              \draw[-,line cap=round] (\bezTwoStart)..controls(\bezTwoControlA)and(\bezTwoControlB)..(\bezTwoEnd);
            \endgroup
          \fi
          \pgfmathsetmacro{\segmamp}{-\segmamp}
          \pgfmathsetmacro{\segmcount}{\segmcount-1}
          \def\segmoffset{0}
        \repeat
        \ifdim.99\pgfdecoratedremainingdistance<\pgfdecoratedinputsegmentlength
          \def\extralength{.1*rnd}
        \else
          \def\extralength{0}
        \fi
        \splitbezier{1/\segmcount}{(\bezTwoStart)}{(\bezTwoControlA)}{(\bezTwoControlB)}{(\bezTwoEnd)}
        \shiftbezier{\hairoffset}{\hairoffset}{(\bezOneStart)}{(\bezOneControlA)}{(\bezOneControlB)}{(\bezOneEnd)}
        \begingroup
          \splitbezier{\segmcount+\extralength}{(\bezOneStart)}{(\bezOneControlA)}{(\bezOneControlB)}{(\bezOneEnd)}
          \draw[-,line cap=round] (\bezOneStart)..controls(\bezOneControlA)and(\bezOneControlB)..(\bezOneEnd);
        \endgroup
        \shiftbezier{\segmamp}{-\segmamp}{(\bezOneStart)}{(\bezOneControlA)}{(\bezOneControlB)}{(\bezOneEnd)}
        \splitbezier{\segmcount+\extralength}{(\bezOneStart)}{(\bezOneControlA)}{(\bezOneControlB)}{(\bezOneEnd)}
        \draw[-,line cap=round] (\bezOneStart)..controls(\bezOneControlA)and(\bezOneControlB)..(\bezOneEnd);
        \global\let\storedsegmoffset\segmcount
        \global\let\storedsegmamp\segmamp
      },
      closepath code={ %% Not yet implemented because the circle is causing (division by ~0?) problems
        % \splitstraighttwice{0.333333}{(\tikzinputsegmentfirst)}{(\tikzinputsegmentlast)}\firstintpoint
        % \splitstraighttwice{0.666667}{(\tikzinputsegmentfirst)}{(\tikzinputsegmentlast)}\secondintpoint
        % \draw[brush hair=#1] (\tikzinputsegmentfirst)..controls(\firstintpoint)and(\secondintpoint)..(\tikzinputsegmentlast);
      },
    }
  }
}
\tikzset{
  brush/.default=orange,
  brush/.code={
    \@tempdima=0.15pt
    \@tempcnta=30
    \loop
      \pgfmathparse{92+8*rnd}
      \begingroup\edef\x{\endgroup\noexpand\tikzset{postaction={brush hair=#1!\pgfmathresult!black,brush hair offset=\the\dimexpr\@tempdima*\@tempcnta}}}\x
      \advance\@tempcnta by -1
      \@tempdima=-\@tempdima
    \ifnum\@tempcnta>-1\repeat
  }
}
\makeatother

\begin{document}\begin{tikzpicture}
        \path[brush=red!90!black] (0,-2) -- (1,0) -- (2,-2);
        \typeout{Legs are FINISHED!}
        \path[bend right=20,brush=orange] (-1,2.5) to (3,2.5);
        \typeout{Arms are FINISHED!}
        \path[brush=blue!50!white] (1,4) circle[radius=1];
        \typeout{Head is FINISHED!}
        \path[brush=green!50!black] (1,0) to[out=80,in=260,looseness=1] (1,3);
        \typeout{Body is FINISHED!}
\end{tikzpicture}\end{document}

Another masterpiece

Explanation and improvements will follow.

Old version

Here is an attempt that works by drawing a lot of oscillating lines of slightly different colours and varying widths. It is incredibly slow, and can likely still be improved in many ways (like user-friendliness, efficiency, legibility, correctness and quality of the result, …).

Usage: \path[brush={hair color=<color>}] (<point A>) -- (<point B>);

You can also change the width of the brush setting brush width=<total width> and there's a bunch of other parameters that can be similarly tweaked. These can all be found at the top of the document below. It shouldn't be difficult to set up a key for the secondary colour (which is currently a slightly darker version of the main colour) as well, but I'm a little Tikz-fatigued at the moment.

\documentclass[tikz,margin=1mm]{standalone}
\usetikzlibrary{decorations}

\pgfkeys{/pgf/decoration/.cd,
         hair color/.initial     = orange, %% <- paint colour
         brush width/.initial    = 10pt,   %% <- total brush stroke width
         step size/.initial      = .5pt,   %% <- length of line segments making up the brush stroke
         hair separation/.initial= .3pt,   %% <- avg. distance between hairs on the brush
         min hair width/.initial = .4pt,   %% <- min. width of the individual hairs
         max hair width/.initial = .6pt,   %% <- max. hair width (it oscillates between these values)
         amplitude/.initial      = .125pt, %% <- max. amplitude of oscillation of hair position (double frequency)
         min period/.initial     = 1cm,    %% <- min. value for the period of both oscillations
         max period/.initial     = 1.2cm,  %% <- max. value for the period of both oscillations
         max overshoot/.initial  = 2pt,    %% <- max. distance hairs can overshoot at the end
         hair offset/.initial    = 0pt,    %% <- only used by the (internal) "brush hair" decoration
}

\makeatletter
\tikzset{
  brush/.code={
    \pgfkeys{/pgf/decoration/.cd,#1}%
    \colorlet{haircolor}{\pgfkeysvalueof{/pgf/decoration/hair color}}%             %% <- primary colour
    \colorlet{darkhaircolor}{haircolor!92!black}%                                  %% <- secondary colour
    \pgfmathsetmacro\br@firststep{\pgfkeysvalueof{/pgf/decoration/brush width}/2}% %% <- first draw outer strokes
    \pgfmathsetmacro\br@secondstep{\br@firststep-\pgfkeysvalueof{/pgf/decoration/hair separation}}% %% <- move inwards
    \global\let\br@postactions\empty              %% <- we'll store postactions here
    \foreach \X in {\br@firststep,\br@secondstep,...,0} {%
      \let\tikz@postactions\br@postactions        %% <- retrieve earlier postactions
      \pgfmathparse{100*rnd}%
      \edef\br@tmp{%                              %% <- we need \pgfmathresult and \X to be expanded
        \noexpand\tikzset{postaction={decorate,decoration={brush hair,hair color=haircolor!\pgfmathresult!darkhaircolor,hair offset=\X}}}%
      }\br@tmp                                    %% <- postaction for a stroke on the upper half
      \pgfmathparse{100*rnd}%
      \edef\br@tmp{%                              %% <- we need \pgfmathresult and \X to be expanded
        \noexpand\tikzset{postaction={decorate,decoration={brush hair,hair color=haircolor!\pgfmathresult!darkhaircolor,hair offset=-\X}}}%
      }\br@tmp                                    %% <- postaction for a stroke on the upper half
      \global\let\br@postactions\tikz@postactions %% <- we need to access this outside the loop
    }%
    \let\tikz@postactions\br@postactions          %% <- "smuggle" postactions out of the loop (yes, this is terrible)
    \pgfmathparse{100*rnd}%
    \tikzset{postaction={decorate,decoration={brush hair,hair color=haircolor!\pgfmathresult!darkhaircolor,hair offset=0}}}%
  }                                               %% ^^ finally add the central stroke
}
\pgfdeclaredecoration{brush hair}{initial}{ %% <- the stroke of a single hair
 \state{initial}[width=0pt, next state=line, persistent precomputation={% %% <- All of this is just set-up
   %% period/overall phase:
   \pgfmathrnd
   \pgfmathsetmacro\br@period{\pgfkeysvalueof{/pgf/decoration/min period}*\pgfmathresult+\pgfkeysvalueof{/pgf/decoration/min period}*(1-\pgfmathresult)}%
   %% ^^ retrieve (random) period of the modulation/oscillation
   \pgfmathrnd
   \edef\br@phase{\pgfmathresult+\noexpand\pgfdecoratedcompleteddistance/\br@period}%
   %% ^^ phase of the modulation/oscillation, depends on how far along the path we are
   %% step size:
   \pgfmathsetmacro\br@stepsize{\pgfkeysvalueof{/pgf/decoration/step size}}%  %% <- retrieve step size
   \pgfmathsetmacro\br@phasestep{\br@stepsize/\br@period}%                    %% <- amount by which phase changes per step
   %% distance from centre:
   \pgfmathsetmacro\br@yoffset{\pgfkeysvalueof{/pgf/decoration/hair offset}}% %% <- overall shift from centre
   \pgfmathsetmacro\br@amp{\pgfkeysvalueof{/pgf/decoration/amplitude}}%       %% <- max. amplitude of modulation
   %% line width:
   \pgfmathsetmacro\br@widthavg{(\pgfkeysvalueof{/pgf/decoration/max hair width}+\pgfkeysvalueof{/pgf/decoration/min hair width})/2}%
   \pgfmathsetmacro\br@widthamp{(\pgfkeysvalueof{/pgf/decoration/max hair width}-\pgfkeysvalueof{/pgf/decoration/min hair width})/2}%
   \pgfmathrnd
   \edef\br@width{\br@widthavg+\br@widthamp*cos(720*(\noexpand\br@phase)+\pgfmathresult)}% %% <- phase-dependent line width
   %% overshoot:
   \pgfmathsetmacro\br@overshoot{\pgfkeysvalueof{/pgf/decoration/max overshoot}}%          %% <- retrieve overshoot value
   %% some other settings
   \color{\pgfkeysvalueof{/pgf/decoration/hair color}}%                                    %% <- retrieve paint color
   \pgfsetarrows{-}%
   \pgfsetroundcap
 }]{ %% <- draw first line segment (random length)
     \pgfmathsetmacro\br@dev{\br@amp*cos(360*\br@phase)+\br@yoffset}%
     \pgfmathsetmacro\br@devnext{\br@amp*cos(360*(\br@phase-\br@phasestep))+\br@yoffset}%
     \pgfmathparse{-rnd*\br@overshoot}%
     \pgfpathmoveto{\pgfqpoint{\pgfmathresult pt}{\br@devnext pt}}%
     \pgfpathlineto{\pgfqpoint{0pt}{\br@dev pt}}%
     \pgfsetlinewidth{\br@width}%
     \pgfusepath{stroke}%
 }
 \state{line}[width=\br@stepsize]{% %% <- draw middle segments (length \br@stepsize)
     \pgfmathsetmacro\br@dev{\br@amp*cos(360*(\br@phase))+\br@yoffset}%                   %% <- amplitude
     \pgfmathsetmacro\br@devnext{\br@amp*cos(360*(\br@phase+\br@phasestep))+\br@yoffset}% %% <- next amplitude
     \pgfpathmoveto{\pgfqpoint{0pt}{\br@dev pt}}%
     \pgfpathlineto{\pgfqpoint{\br@stepsize pt}{\br@devnext pt}}%
     \pgfsetlinewidth{\br@width}% %% <- phase-dependent line width
     \pgfusepath{stroke}%
 }
 \state{final}{% %% <- draw final segment ()
     \pgfmathsetmacro\br@dev{\br@amp*cos(360*(\br@phase))+\br@yoffset}%
     \pgfmathsetmacro\br@devnext{\br@amp*cos(360*(\br@phase+\br@phasestep))+\br@yoffset}%
     \pgfpathmoveto{\pgfqpoint{0pt}{\br@dev pt}}%
     \pgfmathparse{\pgfdecoratedremainingdistance+rnd*\br@overshoot}%
     \pgfpathlineto{\pgfqpoint{\pgfmathresult pt}{\br@devnext pt}}%
     \pgfsetlinewidth{\br@width}%
     \pgfusepath{stroke}%
 }
}
\makeatother

\begin{document}

\begin{tikzpicture}
    \path[brush={hair color=red}] (0,-2) -- (1,0) -- (2,-2);
    \typeout{Legs are finally FINISHED!}
    \path[bend right=20,brush={hair color=orange}] (-1,2.5) to (3,2.5);
    \typeout{Arms are finally FINISHED!}
    \path[bend right=20,brush={hair color=blue!50}] (1,4) circle[radius=1];
    \typeout{Head is finally FINISHED!}
    \path[brush={hair color=green!50}] (1,0) to[bend right=5] (1,1.5) to[bend right=-5] (1,3);
    \typeout{Body is finally FINISHED!}
\end{tikzpicture}

\end{document}

Production of the following image takes a little over 2 minutes on my machine:

A true masterpiece

Explanation (just the conceptual part for now)

I've created a decoration called brush hair. It looks like this

\documentclass[tikz,margin=1mm]{standalone}
\usetikzlibrary{decorations}

<insert long preamble frome before here>

\begin{document}\begin{tikzpicture}
    \path[bend right=20,decorate,decoration={brush hair,hair color=blue!50}] (0,0) -- (1,0);
\end{tikzpicture}\end{document}

A single stroke

Its width is modulated with a random period that is somewhere between 1cm and 1.2cm (random to avoid periodicity of the entire stroke). The minumum width is 0.4pt by default and the maximum is 0.6pt. In addition to this, its centre also oscillates at double this frequency (which may have been a little redundant) with a default amplitude of 0.125pt. It is moreover a little longer than the specified length on both ends, and the amount by which it is longer is, you guessed it, random and can be at most 2pt by default. The initial phases of these oscillations is also random because the entire point of all of this is that it is supposed to look slightly messy/chaotic.

All of the aforementioned numbers can be modified by specifying values for the keys at declared at the top of the above document. The hair offset key moves the entire stroke up (or down) by a specified amount.

A full brush stroke can now be obtained by partially overlaying a large number of brush hair strokes with different offsets. This is precisely what the brush key does: it calls

postaction={decorate,decoration={brush hair,hair color=<…>,offset=<…>}}

for a bunch of different (random) hair colours and a bunch of different offsets (uniformly spread out). The distance between strokes is 0.3pt by default, but also this can be changed. It starts with the strokes at the edges and moves inwards for aesthetic reasons.

  • 2
    +1 It's beautiful, a real work of art, bravo! – AndréC Feb 17 at 14:49
  • 1
    On my system, I didn't have enough memory, I increased it. With main_memory=12000000 it compiles in 137344 ms. That's 2 minutes and 17 seconds. – AndréC Feb 17 at 15:23
  • @AndréC Oh, that's interesting. I didn't expand TeX's memory (or don't remember doing so), so I wonder why it doesn't run out for me. The code is far from optimised (for now I'm just happy that it works), so there is probably still a lot to be gained in terms of speed/memory usage. – Circumscribe Feb 17 at 15:46
  • 2
    @AndréC I've added some comments, but there's a lot going on and I wasn't entirely sure where to start. The comments in the code may not be that easy to read because a lot of these lines are very long. I've reduced the number of strokes somewhat and made them wider, so it should be ~1.5× faster now (but still slow and memory hungry). – Circumscribe Feb 17 at 18:14
  • There's genius in you. May I ask you to take a look here: tex.stackexchange.com/a/49961/138900 – AndréC Feb 17 at 18:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.