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