Robert Eisele
Engineer, Systems Architect and DBA

Creating an Advent Calendar with HTML5 and CSS3

Christmas is near and I wanted to soothe the remaining time till Christmas Eve for my friends with something special. So I thought, why not building an Advent calendar with HTML5 and CSS3 for them, which gets filled with personal things I want to give to them? It turned out to be a very cool project and I wanted to share the thing in order to let you make one yourself for your own friends. If you just want to download the source, you can check it out on github, for everyone else, I'll guide you through the process of making an own Advent calendar.

The best thing on an Advent calendar are the doors with some little things behind them and I wanted to make them look 3D and function accordingly. Using CSS3-transforms and backface-visibility, I created a simple demo which functions with a mouse-hover for now:

The basic idea is that a div called #calendar is spanning the scene and creating the perspective. Inside of it, the door frame sits with the wing in form of the front- and back-face. The contents of the frame remain visible when the door gets opened. As the back of the door is cut out with backface-visibility: hidden only the right portion of contents become visible. As the door is rotated, the contents of the back-face must be rotated around the Y-axis around 180°. The positioning is done with absolute measures, as I wanted to position the doors randomly, but it turned out, it looks much better with a fixed 4x6 grid.

<html>
<head>

<title>Advent Calendar</title>
<style>

#calendar {
    perspective: 1000px;
    position: relative;
    width: 300px;
    height: 300px;
    background: grey;
}

.frame {
    position: absolute;
    cursor: pointer;
    box-shadow: inset 0 0 1px #000;
    background: white;
    left: 100px;
    top: 200px;
    width: 100px;
    height: 100px
}

.door {
    transform-origin: left;
    transform-style: preserve-3d;
    transition: .7s transform cubic-bezier(0.470, 0.000, 0.745, 0.715);
}

.frame:hover .door {
    transform: rotateY(-140deg);
}

.wing {
    position: absolute;
    left: 0px;
    top: 0px;
    border: 1px solid;
    backface-visibility: hidden;
    box-sizing: border-box;
    height:100px;
    width:100px;
}

.front {
    background:grey;
}

.back {
    transform: rotateY(180deg);
    background:blue;
}

</style>
</head>
<body>
    <div id="calendar">
        <div class="frame">
            <div class="door">
                <div class="wing front">Front</div>
                <div class="wing back">Back</div>
            </div>
        </div>
    </div>
</body>
</html>

The next thing was to give the gray background a nice winter theme. I was thinking to make the front-face of every door a canvas and cut out a portion of the original image. The cross-site-policy on how to access images on canvas made me stop that, but it's even simpler by using a background image and for every door, the background image is repeated with background-position.

So, the only things changed so far is the background of the front wing and the calendar itself:

#calendar {
    ...
    background-image:url(themes/general.jpg);
}

.front {
    background-image:url(themes/general.jpg);
    background-position:-101px -201px;
}

.wing {
    background:white; /* necessary to make contents invisible while loading */
}

As we now have the template ready, we can come to the programming part. I created a function calendar, which implements the dynamic creation of calendar doors as follows:

calendar({
  calendar: 'calendar', // Id of div
  height: 750, // Calendar height
  width: 705, // Calendar width
  theme: "foo.jpg" // Background image
}, doors); // Array of doors

The array of doors in the simplest way can be filled with:

var doors = [];
for (var i = 0; i < 24; i++) {

  doors.push({
    num: i + 1,
    backgroundFloor: "white",
    backgroundWing: "blue",
    contentWing: "Door " + i,
    contentFloor: "Door " + i,
    x: (i % 4 | 0) * 190 + 20,
    y: (i / 4 | 0) * 120 + 20,
    width: 95,
    height: 95,
    open: STATE & (1 << i)
  });
}

As I wanted to remember the state, the variable STATE is a bit-vector holding the state of every door, if it's open or closed. The calendar() implementation looks like this:

function calendar(opts, doors) {

  var cal = document.getElementById(opts.calendar);
  cal.style.width = opts.width + "px";
  cal.style.height = opts.height + "px";
  cal.style.backgroundImage = "url(" + opts.theme + ")";

  for (var i = 0; i < doors.length; i++) {

    (function (i) {
      var d = doors[i];

      var frame = document.createElement("div");
      var door = document.createElement("div");
      var cont = document.createElement("div");
      var front = document.createElement("div");
      var back = document.createElement("div");

      frame.style.background = d.backgroundFloor;
      frame.style.left = d.x + "px";
      frame.style.top = d.y + "px";
      frame.style.width = d.width + "px";
      frame.style.height = d.height + "px";
      frame.className = "frame";

      cont.innerHTML = d.contentFloor;
      cont.style.position = "absolute";

      if (d.open) {
        door.className = "door open";
      } else {
        door.className = "door";
      }

      door.onclick = function (ev) {
        ev.stopPropagation();
        changeState(i);
        if (this.className === "door")
          this.className = "door open";
        else
          this.className = "door";
      };

      front.style.backgroundImage = "url(" + opts.theme + ")";
      front.style.backgroundPosition = (-d.x - 1) + "px " + (-d.y - 1) + "px";
      front.style.width = d.width + "px";
      front.style.height = d.height + "px";
      front.style.font = "18px Arial";
      front.style.textAlign = "right";
      front.style.padding = "70px 10px 0px 0px";
      front.className = "wing";
      front.innerHTML = d.num;

      back.style.background = d.backgroundWing;
      back.style.transform = "rotateY(180deg)";
      back.style.width = d.width + "px";
      back.style.height = d.height + "px";
      back.className = "wing";
      back.innerHTML = d.contentWing;

      door.appendChild(front);
      door.appendChild(back);

      frame.appendChild(cont);
      frame.appendChild(door);

      cal.appendChild(frame);

    })(i);
  }
}

On the backend-side not too much must be done. I created an array $gifts, which looks as follows:

$gifts = array(
    'name1' => array(
        array(
            'b' => 'i:path/image.jpg',
            'd' => ''
        ),
        ...
    ),
    'name2' => array(
        ...

With the format being "type:value", with the following types:

  • y: youtube
  • l: link
  • i: image
  • t: text

A variable holding the current day slices the array, which gets pushed to the client, to hinder my all to clever friends to have a look in future doors: array_slice($gifts, 0, $day)

Extra Gimmick

Are we done yet? Of course not. By now, only a static calendar is visible. To make sure my friends are ready for christmas, I added a passcode field, where they need to draw the House of Santa Claus, which is a quite famous game for kids in Germany. The pass-field looks as follows, and only when the correct house was drawn, the calendar becomes visible:

The complete source is on github, linked above.

You might also be interested in the following

Leave a comment

5 * 10