[Tex/LaTex] December challenge: Create an Advent Calendar

funpstricksrandom numberstikz-pgf

While thinking about creating an own Advent Calendar for the beloved people around me – instead of buying a boring one with chocolate in it – a thought came to my mind: "How would I draw this in TikZ?" And since I will go a different way this year and don't have time to realize a LaTeX version, I thought I could make it a challenge for the community.

So, here are the specifications the implementation should provide:

  • On a rectangular field, create 24 randomly distributed "windows" (represented by simple rectangles) that are labelled from 1 through 24.
  • The windows with number 1 through 23 shall be equal in size and aspect ratio, window number 24 can have a different aspect ratio but should cover about twice the area of the other ones.
  • It has to be made sure that no two windows are touching or even overlapping.
  • The placement of the windows shall be computed at runtime via random number generation (not from values somehow hard coded into the document), i.e. every LaTeX run should generate a new arrangement.

The challenge is not restricted to TikZ, every LaTeX package that can be used to accomplish this (pstricks, etc.) is allowed. If you need an idea, how the whole thing could look like, compare the following image. Note that this example differs in two points from the specified criteria: Not all windows are rectangular and not all of them have the same size.

Advent Calendar

So, let the challenge begin and bring the community into X-mas mood…

Best Answer

I'll never be as festive and cheerful as Tom Bombadil so I won't even try. I just want to tackle the minimal problem using only LaTeX.

The following code distributes an arbitrary number of nonintersecting rectangles (with independent sizes) inside a rectangular field. The algorithm is just brute force with a threshold limiting the maximum number of attempts to place a rectangle; if exceeded, an error is thrown.

It's quite gross, but it works relatively well as long as there are just a few windows and these are much smaller than the field. Also, it's just eighty lines.

\documentclass[tikz,border=9]{standalone}

% initialize windows coordinates and sizes
\begingroup\globaldefs=1
  \foreach \n in {1,...,24}{
    \pgfqkeys{/advent/windows/\n}{
      x/.initial=0, w/.initial=1.62cm,
      y/.initial=0, h/.initial=1cm,}}
\endgroup

% double the area of the last one
\pgfqkeys{/advent/windows/24}{
  w=2.29cm, h=1.41cm,}

% define field size
\pgfqkeys{/advent/field}{
  w/.store in=\fW, w=16.2cm,
  h/.store in=\fH, h=10cm,}

% macro to randomly displace a window inside the field
\def\PlaceWindow#1{
  \pgfqkeys{/advent/windows/#1}{
    w/.get=\wW,
    h/.get=\wH,}
  \pgfmathsetmacro{\wX}{random()*(\fW-\wW)+.5*\wW}
  \pgfmathsetmacro{\wY}{random()*(\fH-\wH)+.5*\wH}
  \begingroup\globaldefs=1
    \pgfqkeys{/advent/windows/#1}{
      x/.expand once=\wX,
      y/.expand once=\wY,}
  \endgroup}

% macro to check for collisions and put the result in \CollisionStatus
\def\CheckCollision#1#2{
  \pgfqkeys{/advent/windows/#1}{
    x/.get=\aX, w/.get=\aW,
    y/.get=\aY, h/.get=\aH,}
  \pgfqkeys{/advent/windows/#2}{
    x/.get=\bX, w/.get=\bW,
    y/.get=\bY, h/.get=\bH,}
  \begingroup\globaldefs=1
    \pgfmathsetmacro{\CollisionStatus}%
      {(abs(\aX-\bX)<.5*(\aW+\bW))&&(abs(\aY-\bY)<.5*(\aH+\bH))}
  \endgroup}

% style to draw any window
\pgfkeys{/advent/windows/draw/.style={
  draw, rectangle, line width=2pt, node contents=#1, inner sep=0sp, font=\bf,
  xshift=\pgfkeysvalueof{/advent/windows/#1/x},
  yshift=\pgfkeysvalueof{/advent/windows/#1/y},
  minimum width=\pgfkeysvalueof{/advent/windows/#1/w}-\pgflinewidth,
  minimum height=\pgfkeysvalueof{/advent/windows/#1/h}-\pgflinewidth,},}

% style to draw the field
\pgfkeys{/advent/field/draw/.style={
  draw, rectangle, line width=4pt, node contents={},
  xshift=0.5*\fW, minimum width=\fW+\pgflinewidth,
  yshift=0.5*\fH, minimum height=\fH+\pgflinewidth,},}

% initialize pseudorandom number generator using custom seed if provided
\ifdefined\seed\pgfmathsetseed{\seed}\fi
% distibute windows on the field
\PlaceWindow1
\foreach \n in {2,...,24}{                 % try to place each window
  \foreach \t in {1,...,100}{              % up to a hundred times (THRESHOLD)
    \PlaceWindow\n
    \foreach \k [evaluate=\k using int(\k)] in {2-1,...-1,\n-1}{
      \CheckCollision\n\k                  % until it does not collide
      \if1\CollisionStatus\breakforeach\fi % with the preceeding ones
    }
    \if0\CollisionStatus\breakforeach\fi
  }
  \if1\CollisionStatus\errmessage{Sorry, 100 attempts were not enough.}\fi
}

%draw the thing
\begin{document}\begin{tikzpicture}
  \node[/advent/field/draw];
  \node [font=\Huge\bf] at (.5*\fW,2em+\fH) {Merry whatever.};
  \foreach \n in {1,...,24} \node[/advent/windows/draw=\n];
\end{tikzpicture}\end{document}

Each run gives a different result, as long as you wait a minute: TiKz default seed for pseudorandom numbers is \time*\year. If you want it to change every time just compile using the unix timestamp as a seed:

pdflatex \\def\seed{`date +%s`}\\input main.tex

This also allows for reproducibility. The seed 123 has a runtime of a couple of seconds and outputs

Merry whatever.

Not pretty, but I think the criteria are all satisfied.


UPDATED: aesthetics

I managed to throw some eye candy together while still keeping the code under 150 lines. Since the ordinary Christmas theme is already being adequately developed, I considered the alternative viewpoint.

Here is the code:

\RequirePackage[dvipsnames]{xcolor}
\documentclass[tikz]{standalone}

% initialize windows coordinates and sizes
\begingroup\globaldefs=1
  \foreach \n in {1,...,24}{
    \pgfqkeys{/advent/windows/\n}{
      x/.initial=0, w/.initial=1.62cm,
      y/.initial=0, h/.initial=1cm,}}
\endgroup

% double the area of the last one
\pgfqkeys{/advent/windows/24}{
  w=2.29cm, h=1.41cm,}

% define field size
\pgfqkeys{/advent/field}{
  w/.store in=\fW, w=16.2cm,
  h/.store in=\fH, h=10cm,}

% macro to randomly displace a window inside the field
\def\PlaceWindow#1{
  \pgfqkeys{/advent/windows/#1}{
    w/.get=\wW,
    h/.get=\wH,}
  \pgfmathsetmacro{\wX}{random()*(\fW-\wW)+.5*\wW}
  \pgfmathsetmacro{\wY}{random()*(\fH-\wH)+.5*\wH}
  \begingroup\globaldefs=1
    \pgfqkeys{/advent/windows/#1}{
      x/.expand once=\wX,
      y/.expand once=\wY,}
  \endgroup}

% macro to check for collisions and put the result in \CollisionStatus
\def\CheckCollision#1#2{
  \pgfqkeys{/advent/windows/#1}{
    x/.get=\aX, w/.get=\aW,
    y/.get=\aY, h/.get=\aH,}
  \pgfqkeys{/advent/windows/#2}{
    x/.get=\bX, w/.get=\bW,
    y/.get=\bY, h/.get=\bH,}
  \begingroup\globaldefs=1
    \pgfmathsetmacro{\CollisionStatus}%
      {(abs(\aX-\bX)<.5*(\aW+\bW))&&(abs(\aY-\bY)<.5*(\aH+\bH))}
  \endgroup}

% style to draw any window
\usepackage{contour} \contourlength{.6pt} \contournumber{200}
\pgfkeys{/advent/windows/draw/.style={
  Goldenrod, draw, rectangle, line width=.4pt,
  double=black, double distance=.4pt,
  node contents=\contour{Goldenrod}{\color{black}#1},
  inner sep=0sp, font=\bf\Huge,
  xshift=\pgfkeysvalueof{/advent/windows/#1/x}-.5*\fW,
  yshift=\pgfkeysvalueof{/advent/windows/#1/y}-.5*\fH,
  minimum width=\pgfkeysvalueof{/advent/windows/#1/w}-.6pt,
  minimum height=\pgfkeysvalueof{/advent/windows/#1/h}-.6pt,},}

% initialize pseudorandom number generator using custom seed if provided
\ifdefined\seed\pgfmathsetseed{\seed}\fi
% distibute windows on the field
\PlaceWindow1
\foreach \n in {2,...,24}{                 % try to place each window
  \foreach \t in {1,...,200}{              % up to a hundred times (THRESHOLD)
    \PlaceWindow\n
    \foreach \k [evaluate=\k using int(\k)] in {2-1,...-1,\n-1}{
      \CheckCollision\n\k                  % until it does not collide
      \if1\CollisionStatus\breakforeach\fi % with the preceeding ones
    }
    \if0\CollisionStatus\breakforeach\fi
  }
  \if1\CollisionStatus\errmessage{Sorry, 200 attempts were not enough.}\fi
}

% some tricks
\tikzset{
  moon shading/.code args={fill #1 to #2 then fade to #3}{
    \pgfdeclareradialshading{ring}{\pgfpoint{0cm}{0cm}}%
      {color(0)=(#1);color(25bp*#2)=(#1);color(25bp)=(#3)}
    \pgfkeysalso{/tikz/shading=ring}},
  fuzzy hills action/.style={
    line width=\pgflinewidth+2pt,draw opacity=.1,draw=#1,},
  fuzzy hills recursion/.code 2 args={%
    \pgfmathtruncatemacro{\level}{#1-1}%
    \if0\level\tikzset{preaction={fuzzy hills action=#2}}%
    \else\tikzset{preaction={fuzzy hills action=#2,
                             fuzzy hills recursion={\level}{#2}}}\fi},
  fuzzy hills/.style={
    preaction={fuzzy hills recursion={5}{#1}},draw opacity=1,draw=#1},
}

% draw the thing
\begin{document}\begin{tikzpicture}
  \coordinate [ rectangle, bottom color=violet, top color=black,
     minimum width=\fW+4pt, minimum height=\fH+4pt,];
  \coordinate [ circle, anchor=north, yshift=.5*\fH-8pt, minimum size=0.55*\fH,
    moon shading={fill Goldenrod to 0.8 then fade to BurntOrange},];
  \begin{scope}
    \clip [ rectangle, minimum width=\fW+8pt, minimum height=\fH+8pt];
    \fill [ fuzzy hills=Goldenrod!50!white, fill=purple!10!black]
      (-6cm,-7cm) ellipse (8cm and 6cm);
    \path [ scale=1/12.5, xscale=16.2+.5, yscale=10+.5,
            yshift=-6cm, xshift=-6.4cm,
            fuzzy hills=Goldenrod!50!white, fill=purple!10!black ]
      ( 7.33,8.60) .. controls ( 7.01,8.56) and ( 6.80,8.55) .. 
      ( 6.65,8.56) .. controls ( 6.44,8.41) and ( 6.29,8.35) .. 
      ( 6.16,8.33) .. controls ( 6.01,8.16) and ( 5.86,7.99) .. 
      ( 5.80,7.96) .. controls ( 5.60,7.60) and ( 5.52,7.39) .. 
      ( 5.44,7.17) .. controls ( 5.40,6.96) and ( 5.36,6.75) .. 
      ( 5.34,6.49) .. controls ( 5.35,6.36) and ( 5.36,6.26) .. 
      ( 5.41,5.98) .. controls ( 5.53,5.69) and ( 5.59,5.61) .. 
      ( 5.66,5.50) .. controls ( 5.83,5.49) and ( 5.87,5.39) .. 
      ( 6.18,5.56) .. controls ( 6.36,5.88) and ( 6.35,6.01) .. 
      ( 6.40,6.15) .. controls ( 6.38,6.32) and ( 6.36,6.49) .. 
      ( 6.30,6.69) .. controls ( 6.12,6.91) and ( 6.14,6.90) .. 
      ( 5.90,6.84) .. controls ( 5.74,6.59) and ( 5.72,6.46) .. 
      ( 5.76,6.14) .. controls ( 5.83,6.00) and ( 5.96,5.92) .. 
      ( 6.07,6.07) .. controls ( 6.14,6.30) and ( 6.15,6.26) .. 
      ( 6.05,6.50) --                           ( 5.95,6.33) --
      ( 6.02,6.31) .. controls ( 5.97,6.08) and ( 5.93,6.18) .. 
      ( 5.89,6.29) .. controls ( 5.90,6.39) and ( 5.87,6.45) .. 
      ( 6.00,6.68) .. controls ( 6.09,6.61) and ( 6.22,6.56) .. 
      ( 6.20,6.17) .. controls ( 6.16,6.11) and ( 6.19,5.94) .. 
      ( 5.91,5.84) .. controls ( 5.76,5.94) and ( 5.70,5.98) .. 
      ( 5.61,6.46) .. controls ( 5.65,6.75) and ( 5.74,7.00) .. 
      ( 5.85,7.14) .. controls ( 5.98,7.34) and ( 6.15,7.50) .. 
      ( 6.34,7.44) .. controls ( 6.62,7.36) and ( 6.87,7.03) .. 
      ( 7.05,6.71) .. controls ( 7.18,6.38) and ( 7.22,6.20) .. 
      ( 7.25,6.01) .. controls ( 7.27,5.68) and ( 7.26,5.44) .. 
      ( 7.23,5.26) .. controls ( 7.09,4.77) and ( 6.95,4.31) .. 
      ( 6.78,3.95) .. controls ( 6.59,3.52) and ( 6.39,3.14) .. 
      ( 6.20,2.90) .. controls ( 6.01,2.45) and ( 4.04,0.00) .. 
      ( 3.00,-1.00) --                          (12.49,0.00) --
      (12.48,5.75) .. controls (11.81,5.90) and (11.44,6.16) ..
      (10.71,6.44) .. controls (10.34,6.62) and (10.00,6.76) ..
      ( 9.44,7.10) .. controls ( 9.01,7.40) and ( 8.62,7.78) ..
      ( 8.21,8.23) .. controls ( 7.91,8.33) and ( 7.61,8.43) .. cycle;
  \end{scope}
  \coordinate [ rectangle, draw, line width=8pt,
     minimum width=\fW+\pgflinewidth,
     minimum height=\fH+\pgflinewidth];
  \foreach \n in {1,...,24} \node [ /advent/windows/draw=\n ];
\end{tikzpicture}\end{document}

Here is the output with seed=24122015:

merry nightmares!

What's this? What's this? There's color everywhere! What's this? There's white things in the air! What's this? I can't believe my eyes, I must be dreaming; wake up, Jack, this isn't fair! What's this?

I admit the picture suffers from a distinct lack of spiders, snakes and shrunken heads. Maybe next year I'll get around to drawing some delightful surprises to hide behind the windows.