DEV Community

Cover image for I built a countdown timer without using any JS...or CSS...or HTML ๐Ÿ˜ฒ?? [tutorial on dynamic GIFs for email marketing]
GrahamTheDev
GrahamTheDev

Posted on • Updated on

I built a countdown timer without using any JS...or CSS...or HTML ๐Ÿ˜ฒ?? [tutorial on dynamic GIFs for email marketing]


The GIF below is counting down to the 3rd January 2022 at 09:00. (GMT)


image with text

If the GIF doesn't load you can view an old version of the GIF here.

I think it is pretty cool that for the next 200+ days (at time of writing) that Gif will always be within one or two minutes of an actual countdown timer!

In fact you can check timeanddate.com for the accuracy of my timer here, it should be within a minute when you first load this page!

Obviously it isn't one massive file (it would need 26 MILLION frames! so it would be quite large) so how did I do it?

In this article I cover how I:


Additional info:

As pointed out in a comment by @baboon12 this is a lot of effort for a countdown timer!

The use case is for email marketing, where we cannot use JavaScript to create a countdown and we cannot even use video (reliably), so an animated GIF is our only option!

DO NOT use this on your website!

Downloading a 4mb GIF to display a countdown timer would be a massive mistake for performance, use JavaScript instead (plus it is waaaay easier)!

Finally - if you return to the page the image might not show for you. If this happens refresh the page. I don't think I can fix that as it appears to be the dev.to caching pointing to the old image!

(If this happens you can view an old version of the GIF here).


Right, with all that out of the way, let's begin!

Generating an image sequence with text.

The first part of the puzzle was creating an image sequence that I could later turn into a GIF.

But before we create a sequence of images we have to work out how to make one image!

Creating an image with text over it!

Now with GD image library installed this isn't actually as difficult as it first seems.

Most PHP hosting and environments have GD image library installed so you should be able to do this even on shared hosting!

First we grab the source image (the background we are going to write our text on).

imagecreatefromjpeg('img/inhu-countdown.jpg');
Enter fullscreen mode Exit fullscreen mode

I created a super simple image with a large "white space" to the left.

background image we are working with, blank to left, rocket blasting off to the right in InHu purples, pinks and greys

This is our "canvas" to work on. It also creates the image object that we can work with.

The next thing we need to do is add the text.

For this we use imagettftext.

We need to pass it:

  • The image object
  • The font size (unitless)
  • The angle we want the text at (in degrees)
  • the x position (in pixels from the left);
  • the y position (in pixels from the top);
  • the colour of the text (in RGB - more on that in a sec)
  • the font family (the path to the chosen font)
  • the text (what we want it to say!)

Now the only thing that is a little bit confusing is how you pass RGB colours to the function.

To do that we have to use another function: imagecolorallocate.

This function needs us to pass it:

  • The image object
  • the Red channel value (0-255)
  • the Green channel value (0-255)
  • the Blue channel value (0-255)

Ok that is fine, but I prefer Hex values when working with colours.

No problem, I have a snippet sat in my library for converting Hex to an RGB array!

function hexToRGB($colour) {
        if ($colour[0] == '#') {
            $colour = substr($colour, 1);
        }
        if (strlen($colour) == 6) {
            list( $r, $g, $b ) = array($colour[0] . $colour[1], $colour[2] . $colour[3], $colour[4] . $colour[5]);
        } elseif (strlen($colour) == 3) {
            list( $r, $g, $b ) = array($colour[0] . $colour[0], $colour[1] . $colour[1], $colour[2] . $colour[2]);
        } else {
            return false;
        }
        $r = hexdec($r);
        $g = hexdec($g);
        $b = hexdec($b);
        return array('r' => $r, 'g' => $g, 'b' => $b);
    }
Enter fullscreen mode Exit fullscreen mode

Right so the process is straight forward.

  1. Convert our Hex colour to RGB
  2. Pass our RGB colours to imagecolorallocate to set that colour into the image pallette
  3. Create our text using imagettftext and pass the relevant values.

All in all a simple example might look like this:

// our hex to RGB function
function hexToRGB($colour) {
        if ($colour[0] == '#') {
            $colour = substr($colour, 1);
        }
        if (strlen($colour) == 6) {
            list( $r, $g, $b ) = array($colour[0] . $colour[1], $colour[2] . $colour[3], $colour[4] . $colour[5]);
        } elseif (strlen($colour) == 3) {
            list( $r, $g, $b ) = array($colour[0] . $colour[0], $colour[1] . $colour[1], $colour[2] . $colour[2]);
        } else {
            return false;
        }
        $r = hexdec($r);
        $g = hexdec($g);
        $b = hexdec($b);
        return array('r' => $r, 'g' => $g, 'b' => $b);
    }

//create the image object
$image = imagecreatefromjpeg('img/inhu-countdown.jpg');

// convert our Hex to RGB
$textColour = $this->hexToRGB('#333333');
$textColourImage = imagecolorallocate($image, 
                                      $textColour['r'], 
                                      $textColour['g'], 
                                      $textColour['b']);

// finally create our image
imagettftext($image, 
             24,                   // font size
             0,                    // angle
             150,                  // x coord (150px from left)
             220,                  // y coord (220px from top) 
             $textColourImage,     // colour we allocated earlier 
             'fonts/arial.ttf',    // font path
             "Hello Text!");       // the text we want
Enter fullscreen mode Exit fullscreen mode

Then all we have to do is save the image as our chosen file type.

imagepng($image, "image-with-text");
Enter fullscreen mode Exit fullscreen mode

Adjusting the code so we can create multiple images and add text in different locations.

Right so we have worked out how to create an image with some text on, but that is just one piece of text. We are also hard-wiring all of our values which is obviously useless if we want to dynamically generate a countdown.

Time to turn this into a class we can use.

namespace GifMake;

class gifMake {

    private $image;
    public $texts = array();

    function hexToRGB($colour) {
        if ($colour[0] == '#') {
            $colour = substr($colour, 1);
        }
        if (strlen($colour) == 6) {
            list( $r, $g, $b ) = array($colour[0] . $colour[1], $colour[2] . $colour[3], $colour[4] . $colour[5]);
        } elseif (strlen($colour) == 3) {
            list( $r, $g, $b ) = array($colour[0] . $colour[0], $colour[1] . $colour[1], $colour[2] . $colour[2]);
        } else {
            return false;
        }
        $r = hexdec($r);
        $g = hexdec($g);
        $b = hexdec($b);
        return array('r' => $r, 'g' => $g, 'b' => $b);
    }

    function createImg() {
        $this->image = imagecreatefromjpeg('img/inhu-countdown.jpg');

        foreach ($this->texts AS $item) {

            $fontSize = $item[0];
            $angle = $item[1];
            $x = $item[2];
            $y = $item[3];
            $textColourHex = $item[4];
            $fontFamily = $item[5];
            $text = $item[6];

            $textColourRGB = $this->hexToRGB($textColourHex);
            $textColourImg = imagecolorallocate(
                                    $this->image, 
                                    $textColourRGB['r'], 
                                    $textColourRGB['g'], 
                                    $textColourRGB['b']);


            //add the text
            imagettftext($this->image, 
                         $fontSize, 
                         $angle, 
                         $x, 
                         $y, 
                         $textColor, 
                         $fontFamily, 
                         $text);
        }
        return true;
    }

    function saveAsPng($fileName = 'text-image', $location = '') {
        $fileName = $fileName . ".png";
        $fileName = !empty($location) ? $location . $fileName : $fileName;
        return imagepng($this->image, $fileName);
    }

    function saveAsJpg($fileName = 'text-image', $location = '') {
        $fileName = $fileName . ".jpg";
        $fileName = !empty($location) ? $location . $fileName : $fileName;
        return imagejpeg($this->image, $fileName);
    }

    function showImage() {
        header('Content-Type: image/png');
        return imagepng($this->image);
    }

}
Enter fullscreen mode Exit fullscreen mode

Most of the code is the same as before, but this time we have made adjustments so we can pass values in via an array of "texts" we want adding.

Another thing added are three methods for returning an image (savePng(), saveJpg() and show()) which save the image as a PNG, or save it as a JPG or just output the image to view in the browser (which is useful for testing).

Another thing to notice is that we now have an array declared at the start of the class called $texts. This is where we are going to store each item of text that we want drawn onto the image.

This way we can set and then loop through an array of instructions using foreach ($this->texts AS $item) { to set text in multiple places and multiple colours, sizes etc.

One thing to note is if we wanted to make this truly reusable the image path would need to be set externally, but this is a quick project so hard wiring it is fine for now!

Using our new class!

First we include it in our script and set the namespace

namespace GifMake;
include 'gifmake.php';
Enter fullscreen mode Exit fullscreen mode

Now we can create an image with multiple blocks of text easily:

$img = new gifMake;
$img->texts[] = array(52, 0, 75, 200, "#333333", "font/inhu.ttf", "InHu Launches in...");
$img->texts[] = array(52, 0, 160, 300, "#333333", "font/inhu.ttf", "144");
$img->texts[] = array(52, 0, 426, 300, "#763289", "font/inhu.ttf", "Days");
$img->texts[] = array(52, 0, 160, 380, "#333333", "font/inhu.ttf", "12");
$img->texts[] = array(52, 0, 394, 380, "#763289", "font/inhu.ttf", "Hours");
$img->texts[] = array(52, 0, 160, 460, "#333333", "font/inhu.ttf", "09");
$img->texts[] = array(52, 0, 338, 460, "#763289", "font/inhu.ttf", "Minutes");
$img->texts[] = array(52, 0, 160, 540, "#333333", "font/inhu.ttf", "17");
$img->texts[] = array(52, 0, 300, 540, "#763289", "font/inhu.ttf", "Seconds");

$img->createImg();
$fileName = "img/test";
$img->saveAsPng($fileName);
Enter fullscreen mode Exit fullscreen mode

The output looks something like this:
our test image with the text

Now it took a little bit of work placing the text but other than that it was pretty plain sailing!

The final step for an image sequence

Now that we have a design that works all we have to do now is create a sequence of images where each image has 1 second removed.

I went for a minute (60 images) as a nice balance between file size and people seeing it loop.

So we just have to grab the difference between our target date and now, convert it to days, hours, minutes and seconds and then feed those values into our template we designed earlier.

The following code is a little messy but it was what I ended up with:-


namespace GifMake;
use \GifCreator;
use \Datetime;

include 'giflib.php';
include 'gifcreator.php';

// get our current time and our target time then find the difference.
$now = new DateTime();
$ends = new DateTime('Jan 3, 2022, 09:00:00'); 
$left = $now->diff($ends);

// grab the days, hours, minutes and seconds DIFFERENCE between our two dates
$days = $left->format('%a');
$hours = $left->format('%h');
$minutes = $left->format('%i');
$seconds = $left->format('%s');

// loop 60 times subtracting a second each time and drawing our text
for ($x = 0; $x < 60; $x++) {

    if ($seconds < 0) {
        $seconds = 59;
        $minutes--;
    }
    if ($minutes < 0) {
        $minutes = 59;
        $hours--;
    }
    if ($hours < 0) {
        $hours = 23;
        $days--;
    }

    // we have a check to ensure our countdown date hasn't passed. Useful to add an "else" clause later with a different image for "countdown over"
    if ($now < $ends) {

        $img = new gifMake;
        $img->texts[] = array(26, 0, 38, 100, "#333333", "font/inhu.ttf", "InHu Launches in...");
         // we use str_pad to make sure our days, minutes etc. have at least 2 figures
        $img->texts[] = array(26, 0, 80, 150, "#333333", "font/inhu.ttf", str_pad($days, 2, "0", STR_PAD_LEFT));
        $img->texts[] = array(26, 0, 213, 150, "#763289", "font/inhu.ttf", "Days");
        $img->texts[] = array(26, 0, 80, 190, "#333333", "font/inhu.ttf", str_pad($hours, 2, "0", STR_PAD_LEFT));
        $img->texts[] = array(26, 0, 197, 190, "#763289", "font/inhu.ttf", "Hours");
        $img->texts[] = array(26, 0, 80, 230, "#333333", "font/inhu.ttf", str_pad($minutes, 2, "0", STR_PAD_LEFT));
        $img->texts[] = array(26, 0, 169, 230, "#763289", "font/inhu.ttf", "Minutes");
        $img->texts[] = array(26, 0, 80, 270, "#333333", "font/inhu.ttf", str_pad($seconds, 2, "0", STR_PAD_LEFT));
        $img->texts[] = array(26, 0, 150, 270, "#763289", "font/inhu.ttf", "Seconds");

        // call our build function to add the text and then save it
        $img->createImg();
        $fileName = "img/sequence/img" . $x;
        $img->savePng($fileName);

    }
    $seconds --;
}
Enter fullscreen mode Exit fullscreen mode

Voila! 60 images each with 1 second less than the previous on them

Sequence of 60 images counting down


Creating a GIF

Now I may have made my own little class for adding text, but I wasn't going to write a class for creating GIFs - there are too many things I would need to polish up on!

I found this great GIF creation library by Sybio that seemed super simple.

Here is the entirety of the code required to create our GIF:

$frames = array();
for($x = 0; $x < 60; $x++){
    $frames[] = "img/sequence/img" . $x . ".png";
    $durations[] = 100;
}
// Initialize and create the GIF !
$gc = new GifCreator\GifCreator();
$gc->create($frames, $durations, 10);
$gifBinary = $gc->getGif();
file_put_contents("img/countdown.gif", $gifBinary);
Enter fullscreen mode Exit fullscreen mode

The key part of it is the $gc->create function.

It expects

  • An array of images (I used relative paths but it handles files as well)
  • an array of durations, 1 per frame
  • the number of times to repeat.

One quirk I found was that a duration of 100 is a second, I was expecting 1000 to be a second.

Putting that all together we end up with our GIF

image with text


Updating the dev.to article

Now this one is also pretty straight forward.

To update a dev.to article you need the article ID.

The proper way to get this is to query the API for your articles or store the article ID when we create an article.

But yet again this is a one off project so all I need is to get the ID and hard wire it in.

Luckily you can just:

  • create an article with basic information such as a title (which we can change)
  • save it to draft
  • go to the Dashboard and find the article you just created
  • click the 3 dots for more options and right click on "Archive post" -> inspect.

You will find the ID of your article in the <form> that surrounds the Archive post button id="edit_article_[the ID of your article]".

console showing the location of the ID on the form to grab the article ID

Once we have that ID all we have to do is send a PUT request to
https://dev.to/api/articles/{our-article-id-we-found-earlier} with the following parameters:

  • title - article title
  • published - whether the article is published (true / false)
  • body_markdown - our article content!
  • tags - an array of tags relevant to the article
$vars = array();
$vars['article'] = array();
$vars['article']['title'] = "Your Article Title";
$vars['article']['published'] = false; //set to true to publish
$vars['article']['body_markdown'] = '##Your article markdown';
$vars['article']['tags'] = array('up to', 'four', 'related', 'tags');

$vars_send = json_encode($vars); // convert to JSON

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL,"https://dev.to/api/articles/{your-article-id}");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
curl_setopt($ch, CURLOPT_POSTFIELDS,$vars_send);  //Post Fields

$headers = [
    'api-key: {Your API key - found under settings -> account}',
    'Content-Type: application/json;'
];

curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); // add the headers to the request

curl_exec ($ch);// go go go!
Enter fullscreen mode Exit fullscreen mode

For most purposes that should be sufficient.

However I want the article cover image to be my countdown GIF.

Unfortunately that means we can't send our data as a JSON encoded array of variables, instead we have to send just Markdown and include something called "front matter".

Front matter is basically just meta data about the article.

It's format is quite simple:

---
title: our article title
published: true of false
tags: up to 4 tags that are relevant
description: for twitter cards and open graph (social sharing)
cover_image: URL of the cover image (this is what we need!)
---
**All of our article content**
Enter fullscreen mode Exit fullscreen mode

Once we have that built that we can send just the body markdown (now with our front matter added to the start):

    $vars = array();
    $vars['article'] = array();
    $vars['article']['body_markdown'] = $bodyMarkdownIncludingFrontMatter;

    $vars_send = json_encode($vars);
    [....same as before]
Enter fullscreen mode Exit fullscreen mode

All being well that will complete without errors.

If you do get an error most of the time the API gives a meaningful error message so you know what to fix / what to search for when trying to solve the problem! I had a couple where my markdown was mangled but other than that it was plain sailing....except for a problem that you likely won't encounter under normal use....

An unexpected "gotchya"

I got all of the above working and then ran my update script a few times.

It generated a fresh image, it updated the article correctly....but something wasn't quite right?!

My GIF was not using the latest time for the countdown.

i checked my server - yup the image is generating correctly, so what was the problem?

It turns out that I was being a bit naive! I thought that if I pointed the URL for the image at my server I could control the image.

But dev.to is far cleverer than me and actually grabs the image from my server and caches it. You can't even get around it with classic cache busting techniques like appending ?t=12345 to the end of the image URL.

After a head scratch and a grumble I found a super simple solution.

When I generate the image I just give it a random number as part of the file name.

The only issue with this is that I don't want my server having a new image generated every minute and stored for the next 144 days.

So I also have to remove the old image when I generate a new one.

My final issue is that this actually runs on two separate scripts - one to generate the image and one to update the document (and I want to keep them separate so I can run the create script for the next minute while posting the last post as the creation can take 10-15 seconds).

So in the end I hacked in a quick solution. In the GIF creation script I added the following code:

//delete the existing file
$files = glob('img/countdown-holder/*'); 
foreach($files as $file){ 
  if(is_file($file)) {
    unlink($file);
  }
}

// generate a random number to "bust the cache"
$cacheBuster = rand(1000, 999999999);
//add our file back into the directory with a new file name
file_put_contents("img/countdown-holder/countdown" . $cacheBuster . ".gif", $gifBinary);
Enter fullscreen mode Exit fullscreen mode

And in the article update script I did the following:

$directory = "img/countdown-holder/";
$files = scandir ($directory);
$ourImageURL = $directory . $files[2];
Enter fullscreen mode Exit fullscreen mode

So that we could grab the file with a random name without having to pass information directly between the two scripts.

Would I put this into a mission critical process? No!

Will it do for what I need and probably work without any issues? Yes! and that is good enough for me!

The final bits

Upload the two files to the server.

Go set up a cron job for each of them to run every minute.

Sit and check it all works....and it does if you are reading this article and the timer is in sync to the 1st September 2021 at 09:00 (GMT)!

That is it, hopefully now you know how you can create an image sequence with dynamically added text, stitch those images together to form a GIF and (kind of) know how to update an article using the dev.to API.

I plan on doing a detailed article soon on the dev.to API so if this last section wasn't detailed enough for you then give me a follow and hopefully my API article will help.


So how "in sync" are we

Well if you have actually read the article this far you will probably have noticed the GIFs are either several minutes out or may even have stopped entirely!

However if you refresh the page the GIF should be accurate within a minute, two at the most.

If it wasn't for the caching issue I could have made it perfect to the second with a bit of extra work but you know what, it is close enough for me!


Does it have any practical applications?

You might think with JavaScript being a much better (and far more lightweight!) solution for something like this it is just a waste of time and a "fun project".

But there is one situation where this is useful...Email marketing.

Being able to count down to an offer or special event accurately is quite engaging, and engagement is key! As we can't run JavaScript in emails our only option is a GIF.

There may be other uses but that is why I wanted to learn how to do this as I plan on using it in the future to countdown to any events I am hosting / involved in!


So what am I counting down to?

The launch of my company InHu.

That is all I am going to say for now, you will have to follow me if you want to find out more as I will slowly be releasing details of what I have been planning and orchestrating for the last year ๐Ÿ˜‰!

[deleted user] image

[Deleted User]

Top comments (14)

Collapse
 
grahamthedev profile image
GrahamTheDev

Oh and the very eagle eyed among you might have noticed the font I pass in is called inhu.ttf - if anyone is interested in how I made my own font then let me know and I will write an article on that.

And if animated GIFs are your thing maybe check out this article I wrote on hacking together an animated profile picture for dev.to in less than 30 minutes...things don't always have to be done "the right way" ๐Ÿคฃ:

Collapse
 
yashj9 profile image
yashj9

Yes! font also please

Collapse
 
grahamthedev profile image
GrahamTheDev

No problem, it will be a couple of weeks but I will drop you a DM when I write it! โค

Collapse
 
baboon12 profile image
Bhavya Sura

Too much effort and code for countdown timer ๐Ÿ˜ข๐Ÿ˜ข
But Appreciate your work๐Ÿ”ฅโค๏ธ

Collapse
 
grahamthedev profile image
GrahamTheDev

I agree to an extent but as I said in the section on practical applications it is the only way you could do a countdown timer in emails.

For websites the effort is the least of your worries as the countdown timer weighs in at 4mb for each 1 minute countdown!

Plus the idea is you learn how to write text dynamically to an image - I might put something at the start to explain this to people to avoid confusion! โค๏ธ

Collapse
 
grahamthedev profile image
GrahamTheDev • Edited

Added a disclaimer to the beginning and even changed the title slightly to explain the use case, thanks for pointing out that as I don't want people adding this to their website!! ๐Ÿคฃ๐Ÿคฃ

Collapse
 
afif profile image
Temani Afif

Where is the CSS? where is the CSS ??! I can't breathe ... ๐Ÿ˜ต

Collapse
 
grahamthedev profile image
GrahamTheDev

Oh no, I am so sorry, I completely forgot to put a health warning on it for CSS addicts / ninjas! ๐Ÿ˜œ

Collapse
 
nikhilmwarrier profile image
nikhilmwarrier

You nearly killed me with underCSSization!

Thread Thread
 
grahamthedev profile image
GrahamTheDev

๐Ÿคฃ๐Ÿคฃ adding โ€œunderCSSisationโ€ to my vocabulary (Iโ€™m UK so we spell things โ€œproperlyโ€ over here ๐Ÿ˜œ hehe.) โค๏ธ

Collapse
 
calag4n profile image
calag4n

You are barely insane.

Collapse
 
grahamthedev profile image
GrahamTheDev

Barely insane or barely sane? ๐Ÿ˜‹

I think to be fair at this point you can remove the "barely" part and just call me "completely"...insane. ๐Ÿคฃ

Collapse
 
micahlt profile image
Micah Lindley

Interesting! I'd love to see an implementation of something like this in Node or Deno ๐Ÿ˜Š

Collapse
 
posandu profile image
Posandu

WOW this is insane !! Great work !