Edit in JSFiddle


              
<html>


<head>
  <title>Paranoid — Shiny Demos</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=372, user-scalable=no">
  <link rel="stylesheet" href="styles/style.css">
  <link rel="stylesheet" href="/styles/panel.css">
</head>
<body>
  <div id="container">
    <h1>Paranoid 0.4</h1>
    <canvas id="bouncy" width="360" height="378"></canvas>
    <div id="controls">
      <h2>Instructions</h2>
      <p>To control the paddle, use the mouse, touchscreen or keyboard.</p>
      <dl>
        <dt>(a)</dt>
        <dd>toggle autopilot</dd>
        <dt>(&lt;)</dt>
        <dd>move left</dd>
        <dt>(>)</dt>
        <dd>move right</dd>
      </dl>
    </div>
    <p id="autopilot">Autopilot</p>
  </div>
  <script src="/scripts/modernizr.js"></script>
  <script src="/scripts/panel.js"></script>
  <script src="scripts/options.js"></script>
  <script src="scripts/PxLoader.js"></script>
  <script src="scripts/PxLoaderImage.js"></script>
  <script src="scripts/script.js"></script>


<body>

<script>
/**
 * PixelLab Resource Loader
 * Loads resources while providing progress updates.
 */
function PxLoader(settings) {

    // merge settings with defaults
    settings = settings || {};

    // how frequently we poll resources for progress
    if (settings.statusInterval === null) {
        settings.statusInterval = 5000; // every 5 seconds by default
    }

    // delay before logging since last progress change
    if (settings.loggingDelay === null) {
        settings.loggingDelay = 20 * 1000; // log stragglers after 20 secs
    }

    // stop waiting if no progress has been made in the moving time window
    if (settings.noProgressTimeout === null) {
        settings.noProgressTimeout = Infinity; // do not stop waiting by default
    }

    var entries = [], // holds resources to be loaded with their status
        progressListeners = [],
        timeStarted,
        progressChanged = +new Date();

    /**
     * The status of a resource
     * @enum {number}
     */
    var ResourceState = {
        QUEUED: 0,
        WAITING: 1,
        LOADED: 2,
        ERROR: 3,
        TIMEOUT: 4
    };

    // places non-array values into an array.
    var ensureArray = function(val) {
        if (val === null) {
            return [];
        }

        if (Array.isArray(val)) {
            return val;
        }

        return [ val ];
    };

    // add an entry to the list of resources to be loaded
    this.add = function(resource) {

        // ensure tags are in an array
        resource.tags = ensureArray(resource.tags);

        // ensure priority is set
        if (resource.priority === null) {
            resource.priority = Infinity;
        }

        entries.push({
            resource: resource,
            state: ResourceState.QUEUED
        });
    };

    this.addProgressListener = function(callback, tags) {
        progressListeners.push({
            callback: callback,
            tags: ensureArray(tags)
        });
    };

    this.addCompletionListener = function(callback, tags) {
        progressListeners.push({
            tags: ensureArray(tags),
            callback: function(e) {
                if (e.completedCount === e.totalCount) {
                    callback();
                }
            }
        });
    };

    // creates a comparison function for resources
    var getResourceSort = function(orderedTags) {

        // helper to get the top tag's order for a resource
        orderedTags = ensureArray(orderedTags);
        var getTagOrder = function(entry) {
            var resource = entry.resource,
                bestIndex = Infinity;
            for (var i = 0, len = resource.tags.length; i < len; i++) {
                var index = orderedTags.indexOf(resource.tags[i]);
                if (index >= 0 && index < bestIndex) {
                    bestIndex = index;
                }
            }
            return bestIndex;
        };

        return function(a, b) {
            // check tag order first
            var aOrder = getTagOrder(a),
                bOrder = getTagOrder(b);
            if (aOrder < bOrder) return -1;
            if (aOrder > bOrder) return 1;

            // now check priority
            if (a.priority < b.priority) return -1;
            if (a.priority > b.priority) return 1;
            return 0;
        };
    };

    this.start = function(orderedTags) {
        timeStarted = +new Date();

        // first order the resources
        var compareResources = getResourceSort(orderedTags);
        entries.sort(compareResources);

        // trigger requests for each resource
        for (var i = 0, len = entries.length; i < len; i++) {
            var entry = entries[i];
            entry.status = ResourceState.WAITING;
            entry.resource.start(this);
        }

        // do an initial status check soon since items may be loaded from the cache
        setTimeout(statusCheck, 100);
    };

    var statusCheck = function() {
        var checkAgain = false,
            noProgressTime = (+new Date()) - progressChanged,
            timedOut = (noProgressTime >= settings.noProgressTimeout),
            shouldLog = (noProgressTime >= settings.loggingDelay);

        for (var i = 0, len = entries.length; i < len; i++) {
            var entry = entries[i];
            if (entry.status !== ResourceState.WAITING) {
                continue;
            }

            // see if the resource has loaded
            entry.resource.checkStatus();

            // if still waiting, mark as timed out or make sure we check again
            if (entry.status === ResourceState.WAITING) {
                if (timedOut) {
                    entry.resource.onTimeout();
                }
                else {
                    checkAgain = true;
                }
            }
        }

        // log any resources that are still pending
        if (shouldLog && checkAgain) {
            log();
        }

        if (checkAgain) {
            setTimeout(statusCheck, settings.statusInterval);
        }
    };

    this.isBusy = function() {
        for (var i = 0, len = entries.length; i < len; i++) {
            if (entries[i].status === ResourceState.QUEUED ||
                entries[i].status === ResourceState.WAITING) {
                return true;
            }
        }
        return false;
    };

    // helper which returns true if two arrays share at least one item
    var arraysIntersect = function(a, b) {
        for (var i = 0, len = a.length; i < len; i++) {
            if (b.indexOf(a[i]) >= 0) {
                return true;
            }
        }
        return false;
    };

    var onProgress = function(resource, statusType) {
        // find the entry for the resource
        var entry = null;
        for(var i=0, len = entries.length; i < len; i++) {
            if (entries[i].resource === resource) {
                entry = entries[i];
                break;
            }
        }

        // we have already updated the status of the resource
        if (entry === null || entry.status !== ResourceState.WAITING) {
            return;
        }
        entry.status = statusType;
        progressChanged = +new Date();

        var numResourceTags = resource.tags.length;

        // fire callbacks for interested listeners
        for (var i = 0, numListeners = progressListeners.length; i < numListeners; i++) {
            var listener = progressListeners[i],
                shouldCall;

            if (listener.tags.length === 0) {
                // no tags specified so always tell the listener
                shouldCall = true;
            }
            else {
                // listener only wants to hear about certain tags
                shouldCall = arraysIntersect(resource.tags, listener.tags);
            }

            if (shouldCall) {
                sendProgress(entry, listener);
            }
        }
    };

    this.onLoad = function(resource) {
        onProgress(resource, ResourceState.LOADED);
    };
    this.onError = function(resource) {
        onProgress(resource, ResourceState.ERROR);
    };
    this.onTimeout = function(resource) {
        onProgress(resource, ResourceState.TIMEOUT);
    };

    // sends a progress report to a listener
    var sendProgress = function(updatedEntry, listener) {
        // find stats for all the resources the caller is interested in
        var completed = 0,
            total = 0;
        for (var i = 0, len = entries.length; i < len; i++) {
            var entry = entries[i],
                includeResource;

            if (listener.tags.length === 0) {
                // no tags specified so always tell the listener
                includeResource = true;
            }
            else {
                includeResource = arraysIntersect(entry.resource.tags, listener.tags);
            }

            if (includeResource) {
                total++;
                if (entry.status === ResourceState.LOADED ||
                    entry.status === ResourceState.ERROR ||
                    entry.status === ResourceState.TIMEOUT) {
                    completed++;
                }
            }
        }

        listener.callback({
            // info about the resource that changed
            resource: updatedEntry.resource,

            // should we expose StatusType instead?
            loaded: (updatedEntry.status === ResourceState.LOADED),
            error: (updatedEntry.status === ResourceState.ERROR),
            timeout: (updatedEntry.status === ResourceState.TIMEOUT),

            // updated stats for all resources
            completedCount: completed,
            totalCount: total
        });
    };

    // prints the status of each resource to the console
    var log = this.log = function(showAll) {
        if (!window.console) {
            return;
        }

        var elapsedSeconds = Math.round((+new Date() - timeStarted) / 1000);
        window.console.log('PxLoader elapsed: ' + elapsedSeconds + ' sec');

        for (var i = 0, len = entries.length; i < len; i++) {
            var entry = entries[i];
            if (!showAll && entry.status !== ResourceState.WAITING) {
                continue;
            }

            var message = 'PxLoader: #' + i + ' ' + entry.resource.getName();
            switch(entry.status) {
                case ResourceState.QUEUED:
                    message += ' (Not Started)';
                    break;
                case ResourceState.WAITING:
                    message += ' (Waiting)';
                    break;
                case ResourceState.LOADED:
                    message += ' (Loaded)';
                    break;
                case ResourceState.ERROR:
                    message += ' (Error)';
                    break;
                case ResourceState.TIMEOUT:
                    message += ' (Timeout)';
                    break;
            }

            if (entry.resource.tags.length > 0) {
                message += ' Tags: [' + entry.resource.tags.join(',') + ']';
            }

            window.console.log(message);
        }
    };
}
</script>

<style>
/* typeface: Adore 64 by [email protected] http://www.freakyfonts.de/ */
@font-face {
    font-family: "Adore64";
    src: url(http://media.shinydemos.com/paranoid/Adore64.ttf);
}
html, body { margin: 0; padding: 0; }
body { font: 10px Adore64, sans-serif; background-color: #212121; color: #fff; }
#container { width: 360px; margin: 0 auto 0 auto; padding-top: 24px; position: relative; } 
#container h1,h2 { font-size: 20px; margin: 0; padding: 0; text-transform: uppercase; color: #f00; }
#container h2 { margin-bottom: 8px; }
#container p { margin: 0; padding: 0; }
#container small { font-size: 8px; }
#container a {  color: #f00; }
#container canvas { display: block; margin: 16px 0; border-bottom: 2px #555 solid; cursor: none; }
#container dt { float: left; }
#autopilot { font-size: 16px; text-transform: uppercase; position: absolute; top: 200px; width:360px; text-align:center; background-color: rgba(0,0,0,0.5); }
#container .off { display: none; }

@media (max-height: 500px) {
	h1,h2 { font-size: 12px; }
	#container { font-size: 6px; margin: 0 auto; }
	canvas { margin: 2px 0; }
}

@-o-viewport {
	width: 372px auto;
	orientation: portrait;
	resolution: device;
}

@-ms-viewport {
	width: 372px auto;
	orientation: portrait;
	resolution: device;
}
</style>
</body>
</html>