add script - jscancer - Javascript crap (relatively small)
 (HTM) git clone git://git.codemadness.org/jscancer
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit ba166782d82d92a2dcf4ee22b96da2285874a4b3
 (DIR) parent 9ba8da523384de2bdf895531b8145f93441ceec9
 (HTM) Author: Hiltjo Posthuma <hiltjo@codemadness.org>
       Date:   Thu, 15 Feb 2024 19:18:22 +0100
       
       add script
       
       Diffstat:
         A narrowcasting/README                |      27 +++++++++++++++++++++++++++
         A narrowcasting/index.html            |      48 +++++++++++++++++++++++++++++++
         A narrowcasting/script.js             |     619 +++++++++++++++++++++++++++++++
         A narrowcasting/style.css             |     237 +++++++++++++++++++++++++++++++
       
       4 files changed, 931 insertions(+), 0 deletions(-)
       ---
 (DIR) diff --git a/narrowcasting/README b/narrowcasting/README
       @@ -0,0 +1,27 @@
       +Narrowcasting
       +-------------
       +
       +This is a simple script for some "narrowcasting" screen.
       +It allows to defined HTML elements with data attributes for their configuration.
       +The script will then handle the widgets and slides logic.
       +
       +
       +Some features:
       +
       +* Support embedding of data in <iframe>'s and refresh using some timer.
       +* Support embedding of data in <embed> and update using some timer.
       +* Support updating parts of the screen data using XMLHttpRequest (AJAX) using
       +  some timer.
       +* Date / clock widget, format it with a strftime()-like syntax.
       +  Supports any locale forthe weekday and month names.
       +* Support embedded videos, automatically pausing and continueing them when it
       +  skips to the next slide or looping them.
       +* A mechanism to poll some file for changes using some timer. When this data
       +  changes the page is reloaded (forcing also a cache flush). This is useful for
       +  remotely updating the layouts or scripts.
       +* Show a newsticker. This uses the JSON Feed format.
       +  sfeed_json could be used to convert from RSS/Atom to this format. This data
       +  could be updated using a cronjob.
       +* Progressbar to show the slides remaining duration.
       +* Hotkeys to skip slides, useful for debugging also.
       +* Uses vanilla JS with no dependencies on frameworks, etc.
 (DIR) diff --git a/narrowcasting/index.html b/narrowcasting/index.html
       @@ -0,0 +1,48 @@
       +<!DOCTYPE html>
       +<html>
       +<head>
       +<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
       +<title>Narrowcasting</title>
       +<link rel="stylesheet" type="text/css" href="style.css" />
       +<link rel="icon" type="image/png" href="favicon.png" />
       +<meta name="viewport" content="width=device-width, initial-scale=1.0">
       +<meta http-equiv="X-UA-Compatible" content="IE=edge" />
       +</head>
       +<body>
       +
       +<div class="screen check-updates" data-update="120000" data-url="data/check_updated">
       +        <div class="logo"></div>
       +        <div class="topbar-right">
       +                <span class="datetime time" data-format="%H:%M" data-update="60000"></span>
       +                <span class="datetime date" data-format="<span>%A <b>%e %B</b>" data-update="60000"></span>
       +        </div>
       +
       +        <div class="topbar"></div>
       +
       +        <div class="topbar-info-dashboard"></div>
       +
       +        <div class="slides">
       +                <div class="slide slide-1" id="slide-1" data-displaynext="30000">
       +                        <!-- widgets slide 1 -->
       +                        <div class="widget xhr-content" data-url="data/1" data-timeout="60000" data-update="900000" data-animate-class="visible"></div>
       +                        <div class="widget xhr-content" data-url="data/2" data-update="900000" data-animate-class="visible"></div>
       +                        <div class="widget xhr-content" data-url="data/3" data-update="900000" data-animate-class="visible"></div>
       +
       +                        <div class="widget widget-lichess embed-content" data-update="3600000">
       +                                <iframe src="https://lichess.org/training/frame?theme=green&bg=light" style="width: 400px; height: 444px;" allowtransparency="true" frameborder="0"></iframe>
       +                        </div>
       +
       +                        <div class="news-ticker ticker1" data-url="data/news1.json" data-update="900000" data-fadein="1000" data-displaynext="20000"></div>
       +                        <div class="news-ticker ticker2" data-url="data/news2.json" data-update="900000" data-fadein="1000" data-displaynext="20000"></div>
       +                </div>
       +
       +                <div class="slide slide-2" id="slide-2" data-displaynext="30000" style="background-image: url('http://local/image.png')">
       +                </div>
       +
       +                <div class="progressbar"></div>
       +        </div>
       +</div>
       +<script type="text/javascript" src="script.js">
       +</script>
       +</body>
       +</html>
 (DIR) diff --git a/narrowcasting/script.js b/narrowcasting/script.js
       @@ -0,0 +1,619 @@
       +// run atleast once, then every n milliseconds.
       +// if n is 0 just run once.
       +function updateevery(n, fn) {
       +        // schedule in parallel now.
       +        setTimeout(function() {
       +                fn();
       +        }, 0);
       +        if (!n)
       +                return null;
       +        return setInterval(fn, n);
       +}
       +
       +function pad0(n) {
       +        return (parseInt(n) < 10 ? "0" : "") + n.toString();
       +}
       +
       +function padspace(n) {
       +        return (parseInt(n) < 10 ? " " : "") + n.toString();
       +}
       +
       +// dutch weekdays and months.
       +//var strftime_weekdays = [ "Zondag", "Maandag", "Dinsdag", "Woensdag", "Donderdag", "Vrijdag", "Zaterdag" ];
       +//var strftime_months  = [ "Januari", "Februari", "Maart", "April", "Mei", "Juni", "Juli", "Augustus", "September", "Oktober", "November", "December" ];
       +
       +// subset of strftime()
       +function strftime(fmt, d) {
       +        //var locale = locale || navigator.language || "en";
       +        var locale = "nl-NL"; // dutch
       +        d = d || new Date();
       +
       +        var mon = d.getMonth(); // starts at 0
       +        var day = d.getDay(); // weekday, starts at 0 (sunday).
       +
       +        var s = fmt;
       +        s = s.replace(/%Y/g, d.getFullYear().toString());
       +        s = s.replace(/%m/g, pad0(mon + 1));
       +        s = s.replace(/%d/g, pad0(d.getDate()));
       +        s = s.replace(/%e/g, padspace(d.getDate()));
       +        s = s.replace(/%H/g, pad0(d.getHours()));
       +        s = s.replace(/%M/g, pad0(d.getMinutes()));
       +        s = s.replace(/%S/g, pad0(d.getSeconds()));
       +
       +        s = s.replace(/%A/g, (new Intl.DateTimeFormat(locale, { weekday: "long" })).format(d)); // full weekday name
       +        s = s.replace(/%a/g, (new Intl.DateTimeFormat(locale, { weekday: "short" })).format(d)); // abbreviated weekday name
       +        s = s.replace(/%B/g, (new Intl.DateTimeFormat(locale, { month: "long" })).format(d)); // full month name
       +        s = s.replace(/%b/g, (new Intl.DateTimeFormat(locale, { month: "short" })).format(d)); // abbreviated month name
       +
       +        //s = s.replace(/%A/g, strftime_weekdays[day]); // full weekday name
       +        //s = s.replace(/%a/g, strftime_weekdays[day].substring(0, 3)); // abbreviated weekday name
       +        //s = s.replace(/%B/g, strftime_months[mon]); // full month name
       +        //s = s.replace(/%b/g, strftime_months[mon].substring(0, 3)); // abbreviated month name
       +
       +        return s;
       +}
       +
       +// XMLHttpRequest helper function.
       +function xhr(url, fn, timeout) {
       +        var x = new(XMLHttpRequest);
       +        x.open("get", url, true); // async
       +        x.setRequestHeader("X-Requested-With", "XMLHttpRequest");
       +        x.timeout = parseInt(timeout || 10000); // default: 10 seconds
       +        x.onreadystatechange = function () {
       +                if (x.readyState != 4)
       +                        return;
       +                fn(x);
       +        };
       +        var data = "";
       +        x.send(data);
       +}
       +
       +// find direct descendent child element with a certain classname.
       +function getdirectchilds(parent, classname) {
       +        var els = [];
       +
       +        for (var i = 0; i < parent.children.length; i++) {
       +                var el = parent.children[i];
       +                if (el.classList.contains(classname))
       +                        els.push(el);
       +        }
       +
       +        return els;
       +}
       +
       +// news tickers.
       +var tickerels = document.getElementsByClassName("news-ticker");
       +var tickers = [];
       +for (var i = 0; i < tickerels.length; i++) {
       +        var url = tickerels[i].getAttribute("data-url") || "";
       +        if (url == "")
       +                continue;
       +
       +        // bind element to context
       +        var t = (function(tickerel, delay) {
       +                var displayfn = function(ticker) {
       +                        var parent = ticker.el;
       +                        var items = ticker.items || [];
       +                        var counter = ticker.counter || 0;
       +
       +                        var node = document.createElement("div");
       +                        node.innerText = items[counter] || "";
       +                        parent.appendChild(node); // add new.
       +                        counter = (counter + 1) % items.length;
       +
       +                        ticker.counter = counter; // update counter
       +
       +                        if (parent.children.length) {
       +                                // schedule to be removed.
       +                                setTimeout(function() {
       +                                        if (parent.children.length)
       +                                                parent.children[0].className = "out";
       +                                        setTimeout(function() {
       +                                                if (parent.children.length > 1)
       +                                                        parent.removeChild(parent.children[0]);
       +                                        }, ticker.fadein);
       +                                }, ticker.display);
       +                        }
       +                };
       +
       +                var processfn = function(x) {
       +                        var feed = JSON.parse(x.responseText || "");
       +                        var items = [];
       +                        var feeditems = feed.items || [];
       +
       +                        for (var j = 0; j < feeditems.length; j++) {
       +                                var title = feeditems[j].title;
       +                                var ts = feeditems[j].date_published || "";
       +                                // if there is a timestamp prefix it in the title.
       +                                if (ts.length) {
       +                                        var d = new Date(Date.parse(ts));
       +                                        title = strftime("%H:%M", d) + " " + title;
       +                                }
       +                                items.push(title);
       +                        }
       +                        ticker.items = items;
       +                };
       +
       +                // ticker config / context.
       +                var ticker = {
       +                        el: tickerel,
       +                        url: (tickerel.getAttribute("data-url") || ""),
       +                        display: parseInt(tickerel.getAttribute("data-displaynext")) || 10000, // default: 10 seconds
       +                        fadein: parseInt(tickerel.getAttribute("data-fadein")) || 1000, // default: 1 second
       +                        update: parseInt(tickerel.getAttribute("data-update")) || (15 * 60 * 1000) // default: 15 minutes
       +                };
       +
       +                ticker.processfn = processfn;
       +                ticker.displayfn = displayfn;
       +
       +                // reload RSS/Atom data.
       +                var updater = function() {
       +                        xhr(ticker.url, function(x) {
       +                                ticker.processfn(x);
       +
       +                                // reset and stop previous timer for cycling items.
       +                                if (ticker.displaytimer)
       +                                        clearInterval(ticker.displaytimer);
       +
       +                                ticker.counter = 0; // reset item counter
       +                                ticker.el.innerHTML = ""; // clear contents
       +                                ticker.displayfn(ticker); // immediately update the first time
       +
       +                                // display / cycle existing items.
       +                                ticker.displaytimer = setInterval(function() { ticker.displayfn(ticker); }, ticker.display);
       +                        });
       +                };
       +
       +                ticker.updater = updater;
       +                ticker.updatetimer = setInterval(updater, ticker.update); // update every 15 minutes.
       +                setTimeout(updater, delay); // small delay so the newstickers don't align up.
       +
       +                return ticker;
       +        })(tickerels[i], 1000 * i);
       +
       +        tickers.push(t);
       +}
       +
       +// elements showing simple HTML content in a placeholder, refresh using XMLHTTPRequest (AJAX).
       +var xhrcontentels = document.getElementsByClassName("xhr-content");
       +for (var i = 0; i < xhrcontentels.length; i++) {
       +        var xhrcontentel = xhrcontentels[i];
       +        var url = xhrcontentel.getAttribute("data-url") || "";
       +        if (url == "")
       +                continue;
       +
       +        // bind element to context.
       +        (function(bindel) {
       +                var updatefn = function(config) {
       +                        var parent = config.el;
       +                        var curcontent = parent.childNodes.length ? parent.childNodes[0].innerHTML : "";
       +                        var newcontent = config.data || "";
       +                        // content changed.
       +                        if (curcontent === newcontent)
       +                                return;
       +
       +                        // remove previous nodes.
       +                        while (parent.childNodes.length > 0)
       +                                parent.removeChild(parent.childNodes[0]);
       +
       +                        // add nodes to force a transition.
       +                        var node = document.createElement("div");
       +                        node.innerHTML = newcontent;
       +                        if (config.animate !== "") {
       +                                node.classList.add("animate-once");
       +                                node.classList.add(config.animateclass);
       +                        }
       +                        parent.appendChild(node); // add new.
       +                };
       +
       +                var processfn = function(x) {
       +                        var data = "";
       +                        if (x.status >= 200 && x.status < 400)
       +                                data = x.responseText || "";
       +                        else
       +                                data = ""; // "ERROR";
       +                        config.data = data;
       +                };
       +
       +                // config / context.
       +                var config = {
       +                        animateclass: bindel.getAttribute("data-animate-class"),
       +                        el: bindel,
       +                        processfn: processfn,
       +                        timeout: parseInt(bindel.getAttribute("data-timeout")) || 10000, // default: 10 seconds
       +                        update: parseInt(bindel.getAttribute("data-update")) || (15 * 60 * 1000), // default: 15 minutes
       +                        updatefn: updatefn,
       +                        url: (bindel.getAttribute("data-url") || "")
       +                };
       +
       +                // reload data.
       +                var updater = function() {
       +                        xhr(config.url, function(x) {
       +                                // process data.
       +                                config.processfn(x);
       +                                // update and display data.
       +                                config.updatefn(config);
       +                        }, config.timeout);
       +                };
       +
       +                setInterval(updater, config.update);
       +                // run almost immediately once the first time.
       +                setTimeout(updater, 1);
       +        })(xhrcontentel);
       +}
       +
       +// object or iframe elements that simply show some HTML and refresh in an interval.
       +var embedcontentels = document.getElementsByClassName("embed-content");
       +for (var i = 0; i < embedcontentels.length; i++) {
       +        var embedcontentel = embedcontentels[i];
       +
       +        // must have one child node of type "object" or "iframe".
       +        if (embedcontentel.children.length <= 0)
       +                continue;
       +
       +        var child = embedcontentel.children[0];
       +
       +        // bind element to context.
       +        (function(bindel, child) {
       +                var url = "";
       +                var type = "";
       +                var tagname = child.tagName;
       +                if (tagname === "OBJECT") {
       +                        url = child.data || "";
       +                        type = child.getAttribute("data-type") || "text/html";
       +                } else if (tagname === "IFRAME") {
       +                        url = child.getAttribute("src") || "";
       +                }
       +
       +                // config / context.
       +                var config = {
       +                        el: bindel,
       +                        update: parseInt(bindel.getAttribute("data-update")) || (15 * 60 * 1000), // default: 15 minutes
       +                        url: url,
       +                        type: type
       +                };
       +
       +                var updater;
       +                if (tagname === "OBJECT") {
       +                        // reload data.
       +                        updater = function() {
       +                                var objects = Array.from(bindel.childNodes); // copy nodes.
       +
       +                                var object = document.createElement("object");
       +                                object.data = config.url; // reload
       +                                object.type = config.type;
       +                                object.style.visibility = "hidden";
       +                                object.onload = function() {
       +                                        // quickly swap out the old object(s) and show the new object.
       +                                        // this prevents some flickering.
       +                                        for (var j = 0; j < objects.length; j++)
       +                                                bindel.removeChild(objects[j]);
       +                                        this.style.visibility = "visible";
       +                                };
       +                                bindel.appendChild(object);
       +                        };
       +                } else if (tagname === "IFRAME") {
       +                        // reload data.
       +                        updater = function() {
       +                                child.contentWindow.location.reload();
       +                        };
       +                }
       +
       +                setInterval(updater, config.update);
       +                // run almost immediately once the first time.
       +                setTimeout(updater, 1);
       +        })(embedcontentel, child);
       +}
       +
       +// pause videos inside element.
       +function pausevideos(el) {
       +        var videos = el.getElementsByTagName("video");
       +        for (var i = 0; i < videos.length; i++) {
       +                videos[i].pause();
       +                if ((videos[i].getAttribute("data-reset-on-pause") || "") === "1")
       +                        videos[i].currentTime = 0; // reset time / seek to start.
       +        }
       +}
       +
       +// play / resume videos inside element.
       +function playvideos(el) {
       +        var videos = el.getElementsByTagName("video");
       +        for (var i = 0; i < videos.length; i++) {
       +                videos[i].muted = true; // mute audio by default.
       +                if ((videos[i].getAttribute("data-reset-on-play") || "") === "1")
       +                        videos[i].currentTime = 0; // reset time / seek to start.
       +                videos[i].play(); // NOTE: auto-play must be enabled by the client.
       +        }
       +}
       +
       +// date / clocks.
       +var dates = document.getElementsByClassName("datetime");
       +for (var i = 0; i < dates.length; i++) {
       +        var format = dates[i].getAttribute("data-format") || "";
       +        var freq = dates[i].getAttribute("data-update") || 1000; // default: 1 second
       +
       +        var fn = (function(el, freq, format) {
       +                return function() {
       +                        el.innerHTML = strftime(format); // allow HTML in format.
       +                };
       +        })(dates[i], freq, format);
       +        updateevery(freq, fn);
       +}
       +
       +// init slides and timings.
       +function slides_init(rootel) {
       +        rootel = rootel || document;
       +        var slides = {
       +                current: 0,
       +                prev: -1,
       +                prevprev: -1,
       +                slides: []
       +        };
       +
       +        // find direct descendent element with class "slide".
       +        var slideels = getdirectchilds(rootel, "slide");
       +        for (var i = 0; i < slideels.length; i++) {
       +                var attrtiming = slideels[i].getAttribute("data-displaynext") || "";
       +                var timing = parseInt(attrtiming);
       +                if (timing <= 0)
       +                        timing = 5000;
       +
       +                var slide = {
       +                        timing: timing,
       +                        el: slideels[i]
       +                };
       +                slides.slides.push(slide);
       +        }
       +        return slides;
       +}
       +
       +function slides_showcurrent(slides) {
       +        var el = slides.slides[slides.current].el;
       +        if (el === null)
       +                return;
       +        el.classList.add("visible");
       +        playvideos(el);
       +}
       +
       +function slides_change(slides) {
       +        if (typeof(slides.prev) !== "undefined")
       +                slides.slides[slides.prev].elapsed = 0;
       +        slides.slides[slides.current].elapsed = 0;
       +
       +        slides_showcurrent(slides);
       +        if (slides.onchange)
       +                slides.onchange(slides.prev, slides.prevprev); // current, prev
       +
       +        // completely hidden.
       +        if (slides.prevprev !== -1) {
       +                var el = slides.slides[slides.prevprev].el;
       +                el.classList.remove("pause");
       +        }
       +
       +        // pause / fade out.
       +        if (slides.prev !== -1) {
       +                var el = slides.slides[slides.prev].el;
       +                el.classList.remove("visible");
       +                el.classList.add("pause");
       +                pausevideos(el);
       +        }
       +}
       +
       +function slides_go(slides, n) {
       +        // out-of-bounds or the same: do nothing.
       +        if (n < 0 || n >= slides.slides.length || n == slides.current)
       +                return;
       +        slides.prevprev = slides.prev;
       +        slides.prev = slides.current;
       +        slides.current = n;
       +        slides_change(slides);
       +}
       +
       +function slides_prev(slides) {
       +        var n = slides.current - 1;
       +        if (n < 0 && slides.slides.length)
       +                n = slides.slides.length - 1;
       +        slides_go(slides, n);
       +        slides_change(slides);
       +}
       +
       +function slides_next(slides) {
       +        var n = slides.current + 1;
       +        if (n >= slides.slides.length)
       +                n = 0;
       +        slides_go(slides, n);
       +        slides_change(slides);
       +}
       +
       +function slides_pause(slides) {
       +        slides.paused = true;
       +        clearTimeout(slides.timernextslide);
       +        clearTimeout(slides.timercurrentslide);
       +        pausevideos(slides.slides[slides.current].el);
       +}
       +
       +function slides_play(slides) {
       +        slides_continue(slides);
       +}
       +
       +function slides_continue(slides) {
       +        slides.paused = false;
       +        slides_updater(slides);
       +        slides_showcurrent(slides);
       +}
       +
       +function slides_updater(slides) {
       +        // need more than 1 slide to change it.
       +        if (slides.slides.length <= 1)
       +                return;
       +        var fn = function() {
       +                slides_next(slides);
       +                var currentslide = slides.slides[slides.current];
       +                var timing = currentslide.timing || 5000; // default: 5 seconds
       +                timing -= (currentslide.elapsed || 0); // minus played time for this slide.
       +                slides.timernextslide = setTimeout(fn, timing);
       +        };
       +        var currentslide = slides.slides[slides.current];
       +        var timing = currentslide.timing || 5000; // default: 5 seconds
       +        timing -= (currentslide.elapsed || 0); // minus played time for this slide.
       +        slides.timercurrentslide = setTimeout(fn, timing);
       +}
       +
       +function progressbar_init(slides, progressbar) {
       +        if (slides.slides.length <= 1)
       +                return;
       +
       +        // config: show total progress or progress per slide.
       +        var usetotalprogress = parseInt(progressbar.getAttribute("data-total-progress")) || 0;
       +
       +        function progressbar_update(slides) {
       +                var currentslide = slides.slides[slides.current];
       +                var total = currentslide.timing || 5000; // default: 5 seconds
       +
       +                if (usetotalprogress) {
       +                        total = 0;
       +                        for (var i = 0; i < slides.slides.length; i++) {
       +                                total += parseInt(slides.slides[i].timing) || 0;
       +                        }
       +                }
       +
       +                currentslide.elapsed = currentslide.elapsed || 0;
       +
       +                var perfnow = window.performance.now()
       +                slides.prevprogress = slides.prevprogress || 0;
       +
       +                if (!slides.paused) {
       +                        currentslide.elapsed += perfnow - slides.prevprogress;
       +                }
       +                slides.prevprogress = perfnow;
       +
       +                var elapsed = currentslide.elapsed;
       +                if (usetotalprogress) {
       +                        // current slide progress + all finished previous ones.
       +                        for (var i = 0; i < slides.current; i++)
       +                                elapsed += parseInt(slides.slides[i].timing) || 0;
       +                }
       +
       +                // generate linear gradient with ticks for the total progress.
       +                if (usetotalprogress) {
       +                        var total = 0;
       +                        for (var i = 0; i < slides.slides.length; i++) {
       +                                total += parseInt(slides.slides[i].timing) || 0;
       +                        }
       +
       +                        var pattern = [];
       +                        var timing = 0;
       +                        for (var i = 0; i < slides.slides.length; i++) {
       +                                timing += parseInt(slides.slides[i].timing) || 0;
       +                                var percent = (timing / total) * 100.0;
       +
       +                                pattern.push("rgba(255, 255, 255, 0.0) " + (Math.floor((percent - 0.2) * 100) / 100.0) + "%");
       +
       +                                // don't show tick for last.
       +                                if (i !== slides.slides.length - 1) {
       +                                        // tick color
       +                                        pattern.push("rgba(255,255,255, 1.0) " + (Math.floor((percent) * 100) / 100.0) + "%");
       +                                        pattern.push("rgba(255,255,255, 0.0) " + (Math.floor((percent + 0.2) * 100) / 100.0) + "%");
       +                                }
       +                        }
       +
       +                        // ticks gradient;
       +                        var gradient = "linear-gradient(90deg, " + pattern.join(",") + ")";
       +                        // progress gradient.
       +                        var percent = (elapsed / total) * 100.0;
       +                        gradient += ", linear-gradient(90deg, rgba(255, 255, 255, 0.5) " + percent +
       +                                "%, rgba(255, 255, 255, 0) " + (percent + 0.1) +
       +                                "%, rgba(255, 255, 255, 0) 100%)" // actual progress
       +                        progressbar.style.background = gradient;
       +                        progressbar.style.width = "100%";
       +                } else {
       +                        var percent = (elapsed / total) * 100.0;
       +                        progressbar.style.width = percent + "%";
       +                }
       +        }
       +
       +        function progressbar_updater() {
       +                var fn = function() {
       +                        progressbar_update(slides);
       +                };
       +                setInterval(fn, 16); // 16ms, update at ~60fps
       +        }
       +
       +        progressbar_updater(slides);
       +}
       +
       +// initialize slides, can be nested.
       +var rootels = document.getElementsByClassName("slides") || [];
       +for (var i = 0; i < rootels.length; i++) {
       +        var rootel = rootels[i];
       +
       +        var slides = slides_init(rootel);
       +        slides_play(slides);
       +
       +        /* progressbar: shows current slide progress */
       +        var progressbars = getdirectchilds(rootel, "progressbar");
       +        if (progressbars.length) // use first progressbar.
       +                progressbar_init(slides, progressbars[0]);
       +}
       +
       +/* mechanism to poll if screen data is updated and needs a full refresh
       +  ignoring the cache. */
       +var lasthash = null;
       +var els = document.getElementsByClassName("check-updates");
       +for (var i = 0; i < els.length; i++) {
       +        var el = els[i];
       +        var interval = parseInt(el.getAttribute("data-update"));
       +        if (interval <= 0)
       +                continue;
       +        var url = el.getAttribute("data-url") || "";
       +        if (url === "")
       +                continue;
       +
       +        var fn = function(x) {
       +                if (x.status >= 200 && x.status < 400) {
       +                        var newhash = x.responseText || "";
       +                        if (lasthash === null) {
       +                                lasthash = newhash;
       +                        } else if (newhash !== lasthash) {
       +                                // reload, ignore cache: works in Chrome and Firefox.
       +                                window.location.reload(true);
       +                        }
       +                }
       +        };
       +
       +        setInterval(function() {
       +                xhr(url, fn, 10000);
       +        }, interval);
       +        xhr(url, fn, 10000);
       +
       +        break;
       +}
       +
       +window.addEventListener("keyup", function(e) {
       +        switch (e.keyCode) {
       +        case 32: // space: toggle pause/play.
       +                if (slides.paused)
       +                        slides_continue(slides);
       +                else
       +                        slides_pause(slides);
       +                break;
       +        case 37: // left arrow: previous slide.
       +                slides_prev(slides);
       +                break;
       +        case 13: // return or right arrow next slide.
       +        case 39:
       +                slides_next(slides);
       +                break;
       +        case 49: // '1': go to slide 1
       +        case 50: // '2': go to slide 2
       +        case 51: // '3': go to slide 3
       +        case 52: // '4': go to slide 4
       +        case 53: // '5': go to slide 5
       +        case 54: // '6': go to slide 6
       +        case 55: // '7': go to slide 7
       +        case 56: // '8': go to slide 8
       +        case 57: // '9': go to slide 9
       +                slides_go(slides, e.keyCode - 49);
       +                break;
       +        }
       +}, false);
 (DIR) diff --git a/narrowcasting/style.css b/narrowcasting/style.css
       @@ -0,0 +1,237 @@
       +@keyframes fade-in {
       +        0% {
       +                opacity: 0;
       +        }
       +        100% {
       +                opacity: 1;
       +        }
       +}
       +@keyframes fade-out {
       +        0% {
       +                opacity: 1;
       +        }
       +        100% {
       +                opacity: 0;
       +        }
       +}
       +@keyframes news-slide-in {
       +        0% {
       +                margin-left: 100%;
       +                opacity: 0;
       +        }
       +        100% {
       +                margin-left: 0;
       +                opacity: 1;
       +        }
       +}
       +@keyframes news-slide-out {
       +        0% {
       +                margin-left: 0;
       +                opacity: 1;
       +        }
       +        100% {
       +                opacity: 0;
       +                margin-left: -100%;
       +        }
       +}
       +body {
       +        font-family: sans-serif;
       +}
       +html, body {
       +        overflow: hidden;
       +}
       +h1 {
       +        font-size: 120%;
       +        font-weight: bold;
       +        margin: 5px 0;
       +        padding: 0;
       +}
       +ul {
       +        margin: 0;
       +        padding: 0;
       +}
       +iframe {
       +        border: 0;
       +}
       +.news-ticker {
       +        position: fixed;
       +        background-color: #eee;
       +        line-height: 90px;
       +        height: 90px;
       +        overflow: hidden;
       +}
       +.news-ticker div {
       +        position: absolute;
       +        padding: 0 0 0 5px;
       +        line-height: 90px;
       +        height: 90px;
       +        animation: ease-out news-slide-in 1s;
       +}
       +
       +.news-ticker div.out {
       +        animation: ease-out news-slide-out 2s;
       +}
       +.ticker1 {
       +        z-index: 9999;
       +        bottom: 90px;
       +        left: 0;
       +        right: 0;
       +        font-size: 40px;
       +        background-color: #333;
       +        color: #fff;
       +        font-weight: bold;
       +}
       +.ticker2 {
       +        z-index: 9999;
       +        bottom: 0;
       +        left: 0;
       +        right: 0;
       +        font-size: 40px;
       +        background-color: #555;
       +        color: #fff;
       +        font-weight: bold;
       +}
       +.logo {
       +        width: 854px;
       +        background-image: url('logo.png');
       +        background-repeat: no-repeat;
       +        background-position: center left;
       +        background-size: auto 160px;
       +        height: 180px;
       +        position: absolute;
       +        top: 0;
       +        left: 25px;
       +        z-index: 999;
       +}
       +.screen {
       +        position: absolute;
       +        top: 0;
       +        left: 0;
       +        right: 0;
       +        bottom: 0;
       +
       +        background-repeat: no-repeat;
       +}
       +.topbar {
       +        z-index: 997;
       +
       +        position: absolute;
       +        top: 0;
       +        left: 0;
       +        height: 115px;
       +        right: 0;
       +        background-color: #333;
       +}
       +.topbar-right {
       +        z-index: 999;
       +        position: absolute;
       +        top: 0;
       +        right: 0;
       +        height: 115px;
       +        background-color: #333;
       +        background-repeat: no-repeat;
       +        text-align: left;
       +}
       +
       +.topbar-info-dashboard {
       +        z-index: 999;
       +        position: absolute;
       +        top: 0;
       +        width: 343px;
       +        left: 100px;
       +        height: 158px;
       +        background-image: url('img/image.png');
       +        background-repeat: no-repeat;
       +        text-align: left;
       +}
       +
       +.date {
       +        float: right;
       +        color: #fff;
       +        font-size: 60px;
       +        line-height: 115px;
       +        padding-right: 70px;
       +        white-space: nowrap;
       +}
       +.time {
       +        margin-left: 50px;
       +        padding-right: 50px;
       +        float: right;
       +        color: #fff;
       +        font-weight: bold;
       +        font-size: 90px;
       +        line-height: 115px;
       +        height: 115px;
       +}
       +.slide {
       +        display: none;
       +        background-repeat: no-repeat;
       +        /* default background color, must be set for overlapping slides for transitions */
       +        /*background-color: #fff;*/
       +}
       +
       +body > .screen > .slides > .slide {
       +        position: absolute;
       +        top: 0;
       +        right: 0;
       +        bottom: 0;
       +        left: 0;
       +        height: 100%;
       +        width: 100%;
       +}
       +body > .screen > .slides > .slide-2 {
       +        background-color: #333;
       +}
       +
       +.visible {
       +        z-index: 995; /* visible has more preference over paused */
       +        display: block;
       +
       +        animation: ease-in fade-in 1s;
       +        animation-play-state: running;
       +}
       +
       +.pause {
       +        z-index: 990;
       +        display: block;
       +        opacity: 0;
       +
       +        animation: ease-out fade-out 1s;
       +        animation-play-state: running;
       +}
       +
       +.animate-once {
       +        animation-iteration-count: 1;
       +}
       +.progressbar {
       +        z-index: 9999;
       +        position: absolute;
       +        bottom: 0;
       +        height: 3px;
       +        left: 0;
       +        width: 0%;
       +        background-color: #fff;
       +        opacity: 1.0 !important;
       +}
       +
       +.widget {
       +        padding: 0;
       +        margin: 0;
       +        color: #333;
       +        font-size: 14pt;
       +}
       +.widget-header {
       +        font-size: 30px;
       +        font-weight: bold;
       +        background-repeat: no-repeat;
       +        color: #fff;
       +        line-height: 60px;
       +        padding: 0 10px;
       +        text-transform: uppercase;
       +}
       +.widget-body {
       +        padding: 5px 10px;
       +}
       +.widget-body ul {
       +        padding: 0 20px;
       +}