Publishable Stuff

Rasmus Bååth's Blog


A simple interactive advent calendar in HTML/CSS/JS

2024-11-29

In Sweden, there’s a long tradition of TV and radio advent calendars: shows, often Christmas-themed, that are exactly 24 episodes long. Each of the 24 days in December leading up to Christmas, a new episode drops. Whether this year’s advent calendar shows are better or worse than last year’s is always a hotly debated topic. But, obviously, the best calendar shows were the ones airing when I grew up.

That’s why I was particularly pleased when I won a mint-condition, unopened advent calendar for the 1994 radio show Hertig Hans Slott at an online auction. Instead of keeping this paper calendar unopened, I decided to thoroughly scan it and make it live forever as an online advent calendar. A couple of hours with a friendly AI, and some fidgeting with CSS clip-paths, and I ended up with a result I was pretty happy with:

Here are some notes on how it works and an HTML template that you can use to create a Christmas advent calendar yourself.

An online advent calendar template

To create an online advent calendar using the template at the bottom of this post, you need two things:

  1. An image of the calendar with all the flaps closed and an image with all the flaps removed. These two images need to be perfectly aligned.
  2. Approximate CSS clip-paths that enclose each flap. These are used to mark which parts of the flaps-removed image should be revealed when clicked on.

There are many online tools that can help with creating clip-paths — just search for “clip-path generator” and you will find plenty of them. However, none of them are great, and creating these paths took quite some time.

What didn’t take a lot of time was creating the HTML, CSS, and JavaScript code for the calendar app. As it’s a one-page HTML app, small enough for an LLM AI to keep in context, it just took 1-2 hours to prompt it into existence. (Generally, I’ve been having a lot of fun lately prompting my way to small one-page HTML apps.) The particular LLM I used here was OpenAI’s o1-preview, a so-called reasoning model, that worked really well for my use case.

Below is a template you can use to create your own online advent calendar. If you want to see a fully realized example of this, check the source code of my Hertig Hans Slott advent calendar.

Expand for advent calendar HTML template
<!DOCTYPE html>
<!--
Template for creating an interactive advent calendar web app.
Adjust or replace bits marked with !!!.
Note: For this to work really well, the calendar images, with and without flaps, 
need to be perfectly aligned.
-->

<head>
    <meta charset="UTF-8">
    <!-- Replace with your calendar title -->
    <title>Your Calendar Title Here</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- Replace with a description of your calendar -->
    <meta name="description" content="Description of your calendar.">

    <style>
        body, html {
            margin: 0;
            padding: 0;
            overflow: auto; 
            height: 100%;
            font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
        }

        #calendar-container {
            position: relative;
            width: 100%;
            max-width: 100%; 
            margin: 0 auto;
        }

        #calendar-image {
            width: 100%;
            max-width: 1043.5px; /* !!! Adjust max-width as needed */
            height: auto;
            display: block;
        }

        /* Flap styles */
        .flap {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            cursor: pointer;
            transition: opacity 0.3s ease;
            /* !!! Replace 'YOUR_BOTTOM_LAYER_IMAGE_HERE' with the filename of your bottom layer image */
            background-image: url('YOUR_BOTTOM_LAYER_IMAGE_HERE.jpeg');
            background-size: 100% 100%;
            background-repeat: no-repeat;
            background-position: top left;
            z-index: 100; 
        }

        .flap.shown {
            opacity: 0;
        }
        
        /* "What is this?" link styling */
        #about-link {
            position: absolute;
            bottom: 10px;
            left: 10px;
            color: white;
            background: rgba(0, 0, 0, 0.5);
            padding: 5px 10px;
            text-decoration: none;
            border-radius: 5px;
            font-size: 14px;
            z-index: 500; /* Ensure it's above the flaps */
        }
        
        /* Overlay background */
        #about-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.7);
            display: none; 
            z-index: 1000;
            overflow: auto; 
        }
        
        /* Content box inside the overlay */
        #about-content {
            position: relative;
            background: white;
            margin: 40px auto; 
            padding: 20px;
            width: 80%;
            max-height: calc(100% - 80px); 
            overflow-y: auto; 
            border-radius: 5px;
        }
        
        #close-about {
            position: absolute;
            top: 10px;
            right: 15px;
            font-size: 28px;
            font-weight: bold;
            cursor: pointer;
        }
        
        #close-about:hover {
            color: #669933;
        }

    </style>
    
</head>
<body>

    <div id="calendar-container">
        <!-- !!! Replace 'YOUR_TOP_LAYER_IMAGE_HERE' with your top layer image -->
        <img id="calendar-image" src="YOUR_TOP_LAYER_IMAGE_HERE.jpeg" alt="Your Calendar Alt Text">

        <!-- !!! Flap elements -->
        <!-- For each flap, create a div with class 'flap' and a unique ID, e.g., 'flap1', 'flap2', etc. -->
        <!-- Adjust the 'clip-path' style to define the shape and position of each flap -->
        <!-- To create clip-paths, there are plenty of online tools that can help; just search for 
        "clip-path generator" and you will find many options. -->
        <!-- Example flap: -->
        <!--
        <div class="flap" id="flap1" style="clip-path: circle(6.4% at 12% 9%);"></div>
        -->
        <!-- Repeat for each flap needed in your calendar -->
        
        <!-- "What is this?" link -->
        <!-- Adjust the text as needed -->
        <a href="#" id="about-link">What is this?</a>
    </div>

    <!-- About Overlay -->
    <div id="about-overlay">
        <div id="about-content">
            <span id="close-about">&times;</span>
            <!-- !!! Replace the content below with your own information about the calendar -->
            <h2>Your Calendar Title Here</h2>
            <p>Provide information about your calendar here.</p>
            <!-- Add more content as needed -->
        </div>
    </div>

    <script>
        document.addEventListener('DOMContentLoaded', function() {
            const flaps = document.querySelectorAll('.flap');
            const calendarImage = document.getElementById('calendar-image');
            
            // !!! Create audio objects for open and close sounds
            // Replace 'open.mp3' and 'close.mp3' with your own sound files or remove if not needed
            const openSound = new Audio('open.mp3');
            const closeSound = new Audio('close.mp3');
    
            function adjustFlapSizes() {
                const imageRect = calendarImage.getBoundingClientRect();
                const imageWidth = imageRect.width;
                const imageHeight = imageRect.height;
    
                flaps.forEach((flap) => {
                    flap.style.width = imageWidth + 'px';
                    flap.style.height = imageHeight + 'px';
                });
            }
    
            // Adjust flap sizes on page load
            if (calendarImage.complete) {
                adjustFlapSizes();
            } else {
                calendarImage.addEventListener('load', adjustFlapSizes);
            }
    
            // Adjust flap sizes when the window is resized
            window.addEventListener('resize', adjustFlapSizes);
    
            flaps.forEach((flap) => {
                // !!! Replace 'your_calendar_name' with a unique identifier for your calendar
                const flapShownId = "your_calendar_name_" + flap.id + "_shown";
    
                if (localStorage.getItem(flapShownId) === 'false') {
                    flap.classList.remove('shown');
                } else {
                    flap.classList.add('shown');
                }
    
                // Add click event to toggle flap and update local storage
                flap.addEventListener('click', function() {
                    if (flap.classList.contains('shown')) {
                        flap.classList.remove('shown');
                        localStorage.setItem(flapShownId, 'false');
                        if (openSound) openSound.play();
                    } else {
                        flap.classList.add('shown');
                        localStorage.setItem(flapShownId, 'true');
                        if (closeSound) closeSound.play();
                    }
                });
            });
    
            // Code for the "What is this?" link and overlay
            const aboutLink = document.getElementById('about-link');
            const aboutOverlay = document.getElementById('about-overlay');
            const closeAbout = document.getElementById('close-about');
    
            // Show the overlay when the link is clicked
            aboutLink.addEventListener('click', function(event) {
                event.preventDefault(); // Prevent default link behavior
                aboutOverlay.style.display = 'block';
            });
    
            // Hide the overlay when the close button is clicked
            closeAbout.addEventListener('click', function() {
                aboutOverlay.style.display = 'none';
            });
    
            // Hide the overlay when clicking outside the content box
            aboutOverlay.addEventListener('click', function(event) {
                if (event.target == aboutOverlay) {
                    aboutOverlay.style.display = 'none';
                }
            });
        });
    </script>

</body>
</html>
Posted by Rasmus Bååth | 2024-11-29 | Tags: Programming, Games