38

I recently received an emailer from Onnit Labs that included a Countdown Module Timer inside the emailer using a gif image. The emailer can be viewed here: https://www.onnit.com/emails/lastchance-historic/

The Image can be seen here:

enter image description here

I looked into it, and it seems you can keep sending new frames to an animated GIF using gifsockets, as a GIF doesn't specify how many frames it has when loaded in the browser. Here it is on github: http://github.com/videlalvaro/gifsockets

I thought this was pretty interesting and a cool effect indeed. Does anyone have any other insights on how this could be accomplished? It seems as though the one they're using at Onnit seems to change the countdown according to date appended at the end of URL or image.

onnit.com/emails/_modules/timer/?end=2012-12-27+00:00:00&dark=1

I'm trying to accomplish the same thing to send in an email, but I am a little stumped.

SeanJA
  • 10,234
  • 5
  • 32
  • 42
alvaram
  • 405
  • 1
  • 5
  • 7
  • seems like it ought to just work - just send an html email with an tag whose 'src' attribute references the url. As long as the email reader renders the html properly, it shouldn't matter whether the url is hit by the mail reader or a browser. – GreyBeardedGeek Nov 27 '12 at 22:29
  • 1
    I downloaded the gif that it generated and I only got 60 frames, which may be all they needed? How long was the email? Would you have stuck around for more than 60 seconds? – SeanJA Nov 28 '12 at 01:42
  • I left it open in my browser for a while too, it seems to have lost track of time (I reloaded and it went down by 4 minutes). – SeanJA Nov 28 '12 at 01:43
  • @GreyBeardedGeek well yes it would work, if I use there url to host the image, as like you said I would just include it in the tag. I'm trying to accomplish this with my own image. How would I go about creating something like this? – alvaram Nov 28 '12 at 02:45
  • @SeanJA the gif is endless depending whats appended at the end of the url in the parameters. ?end=2012-12-27+00:00:00 For me it seems to stay on track of the time just fine. Also, reloaded it. – alvaram Nov 28 '12 at 02:49

4 Answers4

37

While maybe gifsockets would work (I haven't tried that before...), there is no network traffic while I am looking at the image other than the initial image load. I am also seeing it it jump from 41 to 42 again. A Reload took it down to 39.

It appears to be just a script that generates 60 frames of animation and sends them to the user. This could probably be done in any language.

Here is how it is done in php:

http://seanja.com/secret/countdown/

enter image description here

SeanJA
  • 10,234
  • 5
  • 32
  • 42
  • Ah, fortunately the stack saves the image on their server, see the live version at the link – SeanJA Nov 28 '12 at 12:14
  • WOW! Amazing, Great Job. This is exactly what I was looking for. I have now implemented what you did here so you can see how I'm using it. http://dev.alvaradom.com/test/countdown/ One question I actually did have is how about the spacing between the characters. I'm using another font, and the spacing was off. How I'm doing it now is just separating the format of text in the php file, like so $text = $interval->format('00 : 00 : 00 : 00'); In the array for font, if there was a spacing parameter('space'=>8,), this would be fantastic! Thanks again! – alvaram Nov 28 '12 at 22:02
  • 1
    That would be convenient ;) . Unfortunately you would have multiple rounds of `imagettftext` like this: https://gist.github.com/4165467 – SeanJA Nov 28 '12 at 23:18
  • Ahh I see, makes sense. I tired looking at this article here for some insights, but I'm not a big PHP guy, so I get kind of lost. Not sure if this can help with the multiple rounds of imagettftext http://stackoverflow.com/questions/6926613/php-imagettftext-letter-spacing But in any case - you answered my question :) Thx! – alvaram Nov 29 '12 at 00:51
  • This is awesome! However, the minutes on this timer are also jumping back and forth. – Jeremiah Prummer Nov 08 '13 at 16:43
  • Ya, they will do that if they open it on the half minute? You can configure it to only count back so many seconds in the code if you want to. – SeanJA Nov 08 '13 at 21:47
  • There seems to be a bug in the PHP code. I loaded the countdown page a few seconds before the hour, and it counted down from "0 days 16 hours 0 minutes 17 seconds" to "0 days 16 hours -1 minutes 17 seconds" or something. – seanf Oct 28 '16 at 11:03
  • 1
    SeanJA Your answer is awesome, which is why I upvoted it a while ago. I also made some improvements you might like: https://stackoverflow.com/a/58250565/470749 I'm wondering if you happen to know how to support transparency? Thanks. – Ryan Oct 05 '19 at 17:07
4

I found http://sendtric.com/ which is free and very easy to integrate.

Guillaume
  • 443
  • 5
  • 11
0

You could try http://makedreamprofits.com/pt/. Instead of supplying additional content to a gif, this countdown is broken into separate images and can count for up to 20 mins without increasing much traffic.

P.S. Gmail is precaching images, so, supplying it endlessly with new frames is not possible.

Eugene
  • 905
  • 1
  • 10
  • 22
  • Yeah, this answer is from 2012 which was pre-gmail caching images, sockets might be a better answer now... I haven't taken a look at this in years. – SeanJA Oct 07 '19 at 14:36
0

I really appreciated Sean Ja's answer. (He deserves more upvotes.) And then I wanted to make the code more readable and configurable (and support text on a transparent gif and automatically center the text):

use Carbon\Carbon;
class CountdownGifHelper {

    const DELAY = 100; /* Why was this labeled as 'milliseconds' when it seems like a value of 100 here causes 1 frame to be shown per second? */
    const MAX_FRAMES = 120;

    /**
     * 
     * @param string $bgImg
     * @param \DateInterval $interval
     * @param array $fontArr
     * @param array $frames
     * @param array $delays
     * @param string $format
     */
    public function addFrame($bgImg, $interval, $fontArr, &$frames, &$delays, $format) {
        $image = imagecreatefrompng($bgImg); //Each frame needs to start by creating a new image because otherwise the new numbers would draw on top of old ones. Here, it doesn't really matter what the PNG is (other than for size) because it's about to get filled with a new color.
        $text = $interval->format($format);
        ob_start();
        imageSaveAlpha($image, true);
        $backgroundColor = $fontArr['backgroundColor'];
        imagefill($image, 0, 0, $backgroundColor); //https://stackoverflow.com/a/17016252/470749 was a helpful hint
        imagecolortransparent($image, $backgroundColor);
        $this->insertCenteredText($image, $fontArr, $text);
        //imagettftext($image, $font['size'], $font['angle'], $font['x-offset'], $font['y-offset'], $font['color'], $font['file'], $text);//this was the old way
        imagegif($image); //The image format will be GIF87a unless the image has been made transparent with imagecolortransparent(), in which case the image format will be GIF89a.
        $frames[] = ob_get_contents();
        ob_end_clean();
        $delays[] = self::DELAY;
    }

    /**
     * 
     * @param resource $image
     * @param array $fontArray
     * @param string $text
     */
    public function insertCenteredText(&$image, $fontArray, $text) {
        $image_width = imagesx($image);
        $image_height = imagesy($image);
        $text_box = imagettfbbox($fontArray['size'], $fontArray['angle'], $fontArray['file'], $text); // Get Bounding Box Size
        $text_width = $text_box[2] - $text_box[0];
        $text_height = $text_box[7] - $text_box[1];
        // Calculate coordinates of the text https://stackoverflow.com/a/14517450/470749
        $x = ($image_width / 2) - ($text_width / 2);
        $y = ($image_height / 2) - ($text_height / 2);
        imagettftext($image, $fontArray['size'], $fontArray['angle'], $x, $y, $fontArray['color'], $fontArray['file'], $text);
    }

    /**
     * 
     * @param int $timestamp
     * @param string $bgImg
     * @param array $fontArray
     * @return string [can be used by Laravel response()->make($gifString, 200, $headers)]
     */
    public function getAnimatedGif($timestamp, $bgImg, $fontArray) {
        $future_date = Carbon::createFromTimestamp($timestamp);
        $time_now = time();
        $moment = new \DateTime(date('r', $time_now));
        $frames = [];
        $delays = [];
        for ($i = 0; $i <= self::MAX_FRAMES; $i++) {
            $interval = date_diff($future_date, $moment);
            if ($future_date < $moment) {
                $this->addFrame($bgImg, $interval, $fontArray, $frames, $delays, '00 : 00 : 00');
                $loops = 1; //stay stuck on this frame
                break;
            } else {
                $this->addFrame($bgImg, $interval, $fontArray, $frames, $delays, '%H : %I : %S');
                $loops = 0; //infinite loop
            }
            $moment->modify('+1 second');
        }
        $animatedGif = new \App\Helpers\AnimatedGif($frames, $delays, $loops, 0, 0, 0);
        return $animatedGif->getAnimation();
    }

    /**
     * ONEDAY allow config via params
     * @param resource $image
     * @return array
     */
    public function getFontArray($image) {
        $fontArr = [
            'file' => resource_path('assets/fonts/Kanit-Regular.ttf'),
            'size' => 30,
            //'x-offset' => 5,
            //'y-offset' => 30,
            'color' => imagecolorallocate($image, 90, 90, 90), //gray
            'backgroundColor' => imagecolorallocate($image, 0, 0, 0), //white. Must match the arguments provided to AnimatedGif (such as 0,0,0).
            'angle' => 0,
        ];
        return $fontArr;
    }

}
Ryan
  • 22,332
  • 31
  • 176
  • 357