1 var jaws = (function(jaws) {
  2   /**
  3    * @fileOverview jaws.assets properties and functions
  4    * 
  5    * Loads and processes image, sound, video, and json assets
  6    * (Used internally by JawsJS to create <b>jaws.assets</b>)
  7    * 
  8    * @class Jaws.Assets
  9    * @constructor
 10    * @property {boolean}    bust_cache              Add a random argument-string to assets-urls when loading to bypass any cache
 11    * @property {boolean}    fuchia_to_transparent   Convert the color fuchia to transparent when loading .bmp-files
 12    * @property {boolean}    image_to_canvas         Convert all image assets to canvas internally
 13    * @property {string}     root                    Rootdir from where all assets are loaded
 14    * @property {array}      file_type               Listing of file postfixes and their associated types
 15    * @property {array}      can_play                Listing of postfixes and (during runtime) populated booleans 
 16    */
 17   jaws.Assets = function Assets() {
 18     if (!(this instanceof arguments.callee))
 19       return new arguments.callee();
 20 
 21     var self = this;
 22 
 23     self.loaded = [];
 24     self.loading = [];
 25     self.src_list = [];
 26     self.data = [];
 27 
 28     self.bust_cache = false;
 29     self.image_to_canvas = true;
 30     self.fuchia_to_transparent = true;
 31     self.root = "";
 32 
 33     self.file_type = {};
 34     self.file_type["json"] = "json";
 35     self.file_type["wav"] = "audio";
 36     self.file_type["mp3"] = "audio";
 37     self.file_type["ogg"] = "audio";
 38     self.file_type['m4a'] = "audio";
 39     self.file_type['weba'] = "audio";
 40     self.file_type['aac'] = "audio";
 41     self.file_type['mka'] = "audio";
 42     self.file_type['flac'] = "audio";
 43     self.file_type["png"] = "image";
 44     self.file_type["jpg"] = "image";
 45     self.file_type["jpeg"] = "image";
 46     self.file_type["gif"] = "image";
 47     self.file_type["bmp"] = "image";
 48     self.file_type["tiff"] = "image";
 49     self.file_type['mp4'] = "video";
 50     self.file_type['webm'] = "video";
 51     self.file_type['ogv'] = "video";
 52     self.file_type['mkv'] = "video";
 53 
 54     self.can_play = {};
 55     
 56     try {
 57       var audioTest = new Audio();
 58       self.can_play["wav"] = !!audioTest.canPlayType('audio/wav; codecs="1"').replace(/^no$/, '');
 59       self.can_play["ogg"] = !!audioTest.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, '');
 60       self.can_play["mp3"] = !!audioTest.canPlayType('audio/mpeg;').replace(/^no$/, '');
 61       self.can_play["m4a"] = !!(audioTest.canPlayType('audio/x-m4a;') || audioTest.canPlayType('audio/aac;')).replace(/^no$/, '');
 62       self.can_play["weba"] = !!audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, '');
 63       self.can_play["aac"] = !!audioTest.canPlayType('audio/aac;').replace(/^no$/, '');
 64       self.can_play["mka"] = !!audioTest.canPlayType('audio/x-matroska;').replace(/^no$/, '');
 65       self.can_play["flac"] = !!audioTest.canPlayType('audio/x-flac;').replace(/^no$/, '');
 66     }
 67     catch(e) {
 68     }
 69 
 70     try {
 71       var videoTest = document.createElement('video');
 72       self.can_play["mp4"] = !!videoTest.canPlayType('video/mp4;').replace(/^no$/, '');
 73       self.can_play["webm"] = !!videoTest.canPlayType('video/webm; codecs="vorbis"').replace(/^no$/, '');
 74       self.can_play["ogv"] = !!videoTest.canPlayType('video/ogg; codecs="vorbis"').replace(/^no$/, '');
 75       self.can_play["mkv"] = !!videoTest.canPlayType('video/x-matroska;').replace(/^no$/, '');
 76     }
 77     catch(e) {
 78     }
 79 
 80     /**
 81      * Returns the length of the resource list
 82      * @public
 83      * @returns {number}  The length of the resource list
 84      */
 85     self.length = function() {
 86       return self.src_list.length;
 87     };
 88 
 89     /**
 90      * Set root prefix-path to all assets
 91      *
 92      * @example
 93      *   jaws.assets.setRoot("music/").add(["music.mp3", "music.ogg"]).loadAll()
 94      *
 95      * @public
 96      * @param   {string} path-prefix for all following assets
 97      * @returns {object} self
 98      */
 99     self.setRoot = function(path) {
100       self.root = path
101       return self
102     }
103  
104     /**
105      * Get one or more resources from their URLs. Supports simple wildcard (you can end a string with "*").
106      *
107      * @example
108      *   jaws.assets.add(["song.mp3", "song.ogg"])
109      *   jaws.assets.get("song.*")  // -> Will return song.ogg in firefox and song.mp3 in IE
110      *
111      * @public
112      * @param   {string|array} src The resource(s) to retrieve 
113      * @returns {array|object} Array or single resource if found in cache. Undefined otherwise.
114      */
115     self.get = function(src) {
116       if (jaws.isArray(src)) {
117         return src.map(function(i) {
118           return self.data[i];
119         });
120       }
121       else if (jaws.isString(src)) {
122         // Wildcard? song.*, match against asset-srcs, make sure it's loaded and return content of first match.
123         if(src[src.length-1] === "*") {
124           var needle = src.replace("*", "")
125           for(var i=0; i < self.src_list.length; i++) {
126             if(self.src_list[i].indexOf(needle) == 0 && self.data[self.src_list[i]]) 
127               return self.data[self.src_list[i]];
128           }
129         }
130         
131         // TODO: self.loaded[src] is false for supported files for some odd reason.
132         if (self.data[src]) { return self.data[src]; } 
133         else                { jaws.log.warn("No such asset: " + src, true); }
134       }
135       else {
136         jaws.log.error("jaws.get: Neither String nor Array. Incorrect URL resource " + src);
137         return;
138       }
139     };
140 
141     /**
142      * Returns if specified resource is currently loading or not
143      * @public 
144      * @param {string} src Resource URL
145      * @return {boolean|undefined} If resource is currently loading. Otherwise, undefined. 
146      */
147     self.isLoading = function(src) {
148       if (jaws.isString(src)) {
149         return self.loading[src];
150       } else {
151         jaws.log.error("jaws.isLoading: Argument not a String with " + src);
152       }
153     };
154 
155     /** 
156      * Returns if specified resource is loaded or not
157      * @param src Source URL
158      * @return {boolean|undefined} If specified resource is loaded or not. Otherwise, undefined.
159      */
160     self.isLoaded = function(src) {
161       if (jaws.isString(src)) {
162         return self.loaded[src];
163       } else {
164         jaws.log.error("jaws.isLoaded: Argument not a String with " + src);
165       }
166     };
167 
168     /**
169      * Returns lowercase postfix of specified resource
170      * @public
171      * @param {string} src Resource URL
172      * @returns {string} Lowercase postfix of resource
173      */
174     self.getPostfix = function(src) {
175       if (jaws.isString(src)) {
176         return src.toLowerCase().match(/.+\.([^?]+)(\?|$)/)[1];
177       } else {
178         jaws.log.error("jaws.assets.getPostfix: Argument not a String with " + src);
179       }
180     };
181 
182     /**
183      * Determine type of file (Image, Audio, or Video) from its postfix
184      * @private
185      * @param {string} src Resource URL
186      * @returns {string} Matching type {Image, Audio, Video} or the postfix itself
187      */
188     function getType(src) {
189       if (jaws.isString(src)) {
190         var postfix = self.getPostfix(src);
191         return (self.file_type[postfix] ? self.file_type[postfix] : postfix);
192       } else {
193         jaws.log.error("jaws.assets.getType: Argument not a String with " + src);
194       }
195     }
196 
197     /**
198      * Add URL(s) to asset listing for later loading
199      * @public
200      * @param {string|array|arguments} src The resource URL(s) to add to the asset listing
201      * @example
202      * jaws.assets.add("player.png")
203      * jaws.assets.add(["media/bullet1.png", "media/bullet2.png"])
204      * jaws.assets.add("foo.png", "bar.png")
205      * jaws.assets.loadAll({onload: start_game})
206      */
207     self.add = function(src) {
208       var list = arguments;
209       if(list.length == 1 && jaws.isArray(list[0])) list = list[0];
210       
211       for(var i=0; i < list.length; i++) {
212         if(jaws.isArray(list[i])) {
213           self.add(list[i]);
214         }
215         else {
216           if(jaws.isString(list[i]))  { self.src_list.push(list[i]) }
217           else                        { jaws.log.error("jaws.assets.add: Neither String nor Array. Incorrect URL resource " + src) }
218         }
219       }
220 
221       return self;
222     };
223 
224     /**
225      * Iterate through the list of resource URL(s) and load each in turn.
226      * @public
227      * @param {Object} options Object-literal of callback functions
228      * @config {function} [options.onprogress] The function to be called on progress (when one assets of many is loaded)
229      * @config {function} [options.onerror] The function to be called if an error occurs
230      * @config {function} [options.onload] The function to be called when finished 
231      */
232     self.loadAll = function(options) {
233       self.load_count = 0;
234       self.error_count = 0;
235 
236       if (options.onprogress && jaws.isFunction(options.onprogress))
237         self.onprogress = options.onprogress;
238 
239       if (options.onerror && jaws.isFunction(options.onerror))
240         self.onerror = options.onerror;
241 
242       if (options.onload && jaws.isFunction(options.onload))
243         self.onload = options.onload;
244 
245       self.src_list.forEach(function(item) {
246         self.load(item);
247       });
248 
249       return self;
250     };
251 
252     /** 
253      * Loads a single resource from its given URL
254      * Will attempt to match a resource to known MIME types.
255      * If unknown, loads the file as a blob-object.
256      * 
257      * @public
258      * @param {string} src Resource URL
259      * @param {Object} options Object-literal of callback functions
260      * @config {function} [options.onload] Function to be called when assets has loaded
261      * @config {function} [options.onerror] Function to be called if an error occurs
262      * @example
263      * jaws.load("media/foo.png")
264      * jaws.load("http://place.tld/foo.png")
265      */
266     self.load = function(src, options) {
267       if(!options) options = {};
268 
269       if (!jaws.isString(src)) {
270         jaws.log.error("jaws.assets.load: Argument not a String with " + src);
271         return;
272       }
273 
274       var asset = {};
275       var resolved_src = "";
276       asset.src = src;
277       asset.onload = options.onload;
278       asset.onerror = options.onerror;
279       self.loading[src] = true;
280       var parser = RegExp('^((f|ht)tp(s)?:)?//');
281       if (parser.test(src)) {
282         resolved_src = asset.src;
283       } else {
284         resolved_src = self.root + asset.src;
285       }
286       if (self.bust_cache) {
287         resolved_src += "?" + parseInt(Math.random() * 10000000);
288       }
289 
290       var type = getType(asset.src);
291       if (type === "image") {
292         try {
293           asset.image = new Image();
294           asset.image.asset = asset;
295           asset.image.addEventListener('load', assetLoaded);
296           asset.image.addEventListener('error', assetError);
297           asset.image.src = resolved_src;
298         } catch (e) {
299           jaws.log.error("Cannot load Image resource " + resolved_src +
300                   " (Message: " + e.message + ", Name: " + e.name + ")");
301         }
302       } 
303       else if (self.can_play[self.getPostfix(asset.src)]) {
304         if (type === "audio") {
305           try {
306             asset.audio = new Audio();
307             asset.audio.asset = asset;
308             asset.audio.addEventListener('error', assetError);
309             asset.audio.addEventListener('canplay', assetLoaded); // NOTE: assetLoaded can be called several times during loading.
310             self.data[asset.src] = asset.audio;
311             asset.audio.src = resolved_src;
312             asset.audio.load();
313           } catch (e) {
314             jaws.log.error("Cannot load Audio resource " + resolved_src +
315                     " (Message: " + e.message + ", Name: " + e.name + ")");
316           }
317         } 
318       else if (type === "video") {
319           try {
320             asset.video = document.createElement('video');
321             asset.video.asset = asset;
322             self.data[asset.src] = asset.video;
323             asset.video.setAttribute("style", "display:none;");
324             asset.video.addEventListener('error', assetError);
325             asset.video.addEventListener('canplay', assetLoaded);
326             document.body.appendChild(asset.video);
327             asset.video.src = resolved_src;
328             asset.video.load();
329           } catch (e) {
330             jaws.log.error("Cannot load Video resource " + resolved_src +
331                     " (Message: " + e.message + ", Name: " + e.name + ")");
332           }
333         }
334       }
335       
336       //Load everything else as raw blobs...
337       else {
338         // ... But don't load un-supported audio-files.
339         if(type === "audio" && !self.can_play[self.getPostfix(asset.src)]) {
340           assetSkipped(asset);
341           return self;
342         }
343 
344         try {
345           var req = new XMLHttpRequest();
346           req.asset = asset;
347           req.onreadystatechange = assetLoaded;
348           req.onerror = assetError;
349           req.open('GET', resolved_src, true);
350           if (type !== "json")
351             req.responseType = "blob";
352           req.send(null);
353         } catch (e) {
354           jaws.log.error("Cannot load " + resolved_src +
355                   " (Message: " + e.message + ", Name: " + e.name + ")");
356         }
357       }
358       
359       return self;
360     };
361 
362     /** 
363      * Initial loading callback for all assets for parsing specific filetypes or
364      *  optionally converting images to canvas-objects.
365      * @private
366      * @param {EventObject} event The EventObject populated by the calling event
367      * @see processCallbacks()
368      */
369     function assetLoaded(event) {
370       var asset = this.asset;
371       var src = asset.src;
372       var filetype = getType(asset.src);
373       
374       try {
375         if (filetype === "json") {
376           if (this.readyState !== 4) {
377             return;
378           }
379           self.data[asset.src] = JSON.parse(this.responseText);
380         }
381         else if (filetype === "image") {
382           var new_image = self.image_to_canvas ? jaws.imageToCanvas(asset.image) : asset.image;
383           if (self.fuchia_to_transparent && self.getPostfix(asset.src) === "bmp") {
384             new_image = fuchiaToTransparent(new_image);
385           }
386           self.data[asset.src] = new_image;
387         }
388         else if (filetype === "audio" && self.can_play[self.getPostfix(asset.src)]) {
389           self.data[asset.src] = asset.audio;
390         }
391         else if (filetype === "video" && self.can_play[self.getPostfix(asset.src)]) {
392           self.data[asset.src] = asset.video;
393         } else {
394           self.data[asset.src] = this.response;
395         }
396       } catch (e) {
397         jaws.log.error("Cannot process " + src +
398                   " (Message: " + e.message + ", Name: " + e.name + ")");
399         self.data[asset.src] = null;
400       }
401 
402       /*
403       * Only increment load_count ONCE per unique asset.
404       * This is needed cause assetLoaded-callback can in certain cases be called several for a single asset...
405       * ..and not only Once when it's loaded.
406       */
407       if( !self.loaded[src]) self.load_count++;
408 
409       self.loaded[src] = true;
410       self.loading[src] = false;
411 
412       processCallbacks(asset, true, event);
413     }
414     
415     /** 
416      * Called when jaws asset-handler decides that an asset shouldn't be loaded
417      * For example, an unsupported audio-format won't be loaded.
418      *
419      * @private
420     */
421     function assetSkipped(asset) {
422       self.loaded[asset.src] = true;
423       self.loading[asset.src] = false;
424       self.load_count++;
425       processCallbacks(asset, true);
426     }
427 
428     /**
429      * Increases the error count and calls processCallbacks with false flag set
430      * @see processCallbacks()
431      * @private 
432      * @param {EventObject} event The EventObject populated by the calling event
433      */
434     function assetError(event) {
435       var asset = this.asset;
436       self.error_count++;
437       processCallbacks(asset, false, event);
438     }
439 
440     /** 
441      * Processes (if set) the callbacks per resource
442      * @private
443      * @param {object} asset The asset to be processed
444      * @param {boolean} ok If an error has occured with the asset loading
445      * @param {EventObject} event The EventObject populated by the calling event
446      * @see jaws.start() in core.js
447      */
448     function processCallbacks(asset, ok, event) {
449       var percent = parseInt((self.load_count + self.error_count) / self.src_list.length * 100);
450 
451       if (ok) {
452         if(self.onprogress)
453           self.onprogress(asset.src, percent);
454         if(asset.onprogress && event !== undefined)
455           asset.onprogress(event);
456       }
457       else {
458         if(self.onerror)
459           self.onerror(asset.src, percent);
460         if(asset.onerror && event !== undefined)
461           asset.onerror(event);
462       }
463 
464       if (percent === 100) {
465         if(self.onload) self.onload();
466 
467         self.onprogress = null;
468         self.onerror = null;
469         self.onload = null;
470       }
471     }
472 
473     /**
474      * Displays the progress of asset handling as an overall percentage of all loading
475      * (Can be overridden as jaws.assets.displayProgress = function(percent_done) {})
476      * @public
477      * @param {number} percent_done The overall percentage done across all resource handling
478      */
479     self.displayProgress = function(percent_done) {
480 
481       if (!jaws.isNumber(percent_done))
482         return;
483 
484       if (!jaws.context)
485         return;
486 
487       jaws.context.save();
488       jaws.context.fillStyle = "black";
489       jaws.context.fillRect(0, 0, jaws.width, jaws.height);
490 
491       jaws.context.fillStyle = "white";
492       jaws.context.strokeStyle = "white";
493       jaws.context.textAlign = "center";
494 
495       jaws.context.strokeRect(50 - 1, (jaws.height / 2) - 30 - 1, jaws.width - 100 + 2, 60 + 2);
496       jaws.context.fillRect(50, (jaws.height / 2) - 30, ((jaws.width - 100) / 100) * percent_done, 60);
497 
498       jaws.context.font = "11px verdana";
499       jaws.context.fillText("Loading... " + percent_done + "%", jaws.width / 2, jaws.height / 2 - 35);
500 
501       jaws.context.font = "11px verdana";
502       jaws.context.fillStyle = "#ccc";
503       jaws.context.textBaseline = "bottom";
504       jaws.context.fillText("powered by www.jawsjs.com", jaws.width / 2, jaws.height - 1);
505 
506       jaws.context.restore();
507     };
508   };
509 
510   /** 
511    * Make Fuchia (0xFF00FF) transparent (BMPs ONLY)
512    * @private
513    * @param {HTMLImageElement} image The Bitmap Image to convert
514    * @returns {CanvasElement} canvas The translated CanvasElement 
515    */
516   function fuchiaToTransparent(image) {
517     if (!jaws.isDrawable(image))  
518       return;
519 
520     var canvas = jaws.isImage(image) ? jaws.imageToCanvas(image) : image;
521     var context = canvas.getContext("2d");
522     var img_data = context.getImageData(0, 0, canvas.width, canvas.height);
523     var pixels = img_data.data;
524     for (var i = 0; i < pixels.length; i += 4) {
525       if (pixels[i] === 255 && pixels[i + 1] === 0 && pixels[i + 2] === 255) { // Color: Fuchia
526         pixels[i + 3] = 0; // Set total see-through transparency
527       }
528     }
529 
530     context.putImageData(img_data, 0, 0);
531     return canvas;
532   }
533 
534   jaws.assets = new jaws.Assets();
535   return jaws;
536 })(jaws || {});
537 
538