source/class/core/io/Asset.js
/*
==================================================================================================
Core - JavaScript Foundation
Copyright 2010-2012 Zynga Inc.
Copyright 2012-2014 Sebastian Werner
==================================================================================================
*/
"use strict";
(function(global, Object)
{
/** {Map} Internal cache for holding preloaded data */
var cache = {};
/**
* {Map} Collects asset with the given asset @prefix {String} from the given @section {Map}.
* Optionally works recursively by enabling @recursive {Boolean?false}. Last parameter @entries {Map?}
* is used to fill an existing object instead of a new one.
*/
var collect = function(prefix, section, recursive, entries)
{
if (!entries) {
entries = {};
}
if (section)
{
for (var filename in section)
{
var entry = section[filename];
var id = prefix + "/" + filename;
// Quite lightweight check: When there is a profile key, we handle it as an entry
if (typeof entry.p == "number") {
entries[id] = entry;
} else if (recursive) {
collect(id, entry, recursive, entries);
}
}
}
return entries;
};
/**
* {Integer} Returns the number of frames for the given @data {Array}.
*/
var getFrameNumber = function(data)
{
// Data format is [width, height, spriteData, animationData]
var animationData = data[3];
// Correct entry length for format detection
if (animationData)
{
switch(animationData.length)
{
case 1:
// manually defined layout/frames
return animationData[0].length;
case 2:
// auto calculated frame size (based on columns and rows)
return animationData[0] * animationData[1];
case 3:
// manually defined frame size
return animationData[2];
}
}
return 1;
};
/**
* {var} Returns a @key {String} from the given asset @id {String} when its available in cache.
*/
var getFromCache = function(id, key)
{
var cached = cache[id];
if (cached && key in cached) {
return cached[key];
} else {
return null;
}
};
/**
* Contains information about images (size, format, clipping, ...) and
* other assets like CSS files, local data, ...
*
* Works with data delivered by {jasy.Asset} and extends it with useful
* tools to make features like animations and sprites easier to use.
*/
core.Module("core.io.Asset",
{
/**
* Loads the given @section {String}. Optionally @recursive {Boolean?false} where it traverses
* the whole tree. The optional @callback {Function?} is executed
* in the given @context {Object?} when all files are loaded. It is called with one
* parameter which contains are `Map` of all data (relative asset ID to loaded item).
* Optionally caching can be disabled by attaching a random `GET` parameter via
* setting @random {Boolean} to `true`.
*/
preloadSection: function(section, recursive, callback, context, random)
{
if (jasy.Env.isSet("debug"))
{
core.Assert.isType(section, "String");
if (section === "") {
throw new Error("Invalid section: " + section);
}
if (core.String.endsWith(section, "/")) {
throw new Error("Sections must not end with a slash!");
}
if (recursive != null) {
core.Assert.isType(recursive, "Boolean");
}
if (callback != null) {
core.Assert.isType(callback, "Function");
}
if (context != null) {
core.Assert.isType(context, "Object");
}
if (random != null) {
core.Assert.isType(random, "Boolean");
}
}
// Collect asset entries
var entries = collect(section, jasy.Asset.resolve(section), recursive);
// Resolve URIs for assets
var uris = [];
var uriToId = {};
for (var id in entries)
{
// Filter loaded assets
if (!(id in cache))
{
var entry = entries[id];
// Don't preload audio/fonts/video
// - The formats for video/audio is typcially streamed.
// - Mostly offered files are alternative e.g. aac/mp3, ttf/woff, m4v/webm, etc.
if (entry.t == "a" || entry.t == "f" || entry.t == "v") {
continue;
}
// Check whether entry is type of image and is part of a sprite image
if (entry.t == "i" && entry.d[2])
{
id = jasy.Asset.resolveSprite(entry.d[2][0], id);
// Omit loading sprite multiple times
if (id in cache) {
continue;
}
var uri = jasy.Asset.toUri(id);
}
else
{
var uri = jasy.Asset.entryToUri(entry, id);
}
uris.push(uri);
uriToId[uri] = id;
// Pre-fill cache to mark as blocked for further calls
cache[id] = true;
}
}
if (uris.length === 0)
{
// Execute user defined callback method
if (callback) {
callback.call(context||global, core.Object.getKeys(entries));
}
}
else
{
if (jasy.Env.isSet("debug")) {
console.debug("Preloading " + section + " (" + uris.length + " assets)...");
}
// Start loading of assets
core.io.Queue.load(uris, function(data)
{
// Fill cache with actual data
for (var uri in data) {
cache[uriToId[uri]] = data[uri];
}
// Execute user defined callback method
if (callback) {
callback.call(context||global, core.Object.getKeys(entries));
}
}, this, random);
}
},
/**
* Loads the given assets by their @ids {String[]} and executes @callback {Function?}
* in the given @context {Object?global}. * Optionally caching can be disabled
* by attaching a random `GET` parameter via setting @random {Boolean} to `true`.
*/
load: function(ids, callback, context, random)
{
if (jasy.Env.isSet("debug"))
{
core.Assert.isType(ids, "Array");
if (callback != null) {
core.Assert.isType(callback, "Function");
}
if (context != null) {
core.Assert.isType(context, "Object");
}
if (random != null) {
core.Assert.isType(random, "Boolean");
}
}
var uris = ids.map(jasy.Asset.toUri);
var urisToIds = uris.zip(ids);
var helper = callback ? function(data) {
callback.call(context||global, core.Object.translate(data, urisToIds));
} : callback;
core.io.Queue.load(uris, helper, this, random);
},
/**
* {Array} Returns the dimensions of the given image @id {String} with as `width`, `height`.
*/
getImageSize : function(id)
{
if (jasy.Env.isSet("debug")) {
core.Assert.isType(id, "String", "Invalid asset ID (no string): " + id + "!");
}
var entry = jasy.Asset.resolve(id);
if (jasy.Env.isSet("debug"))
{
if (!entry) {
throw new Error("Could not figure out size of unknown image: " + id);
}
if (entry.t != "i") {
throw new Error("Could not figure out size of non image asset: " + id);
}
}
// First two values in data are the size of the image
return entry.d.slice(0, 2);
},
/**
* {Integer} Returns the number of frames (for animations) for the given image @id {String}.
*/
getFrameNumber: function(id)
{
if (jasy.Env.isSet("debug")) {
core.Assert.isType(id, "String", "Invalid asset ID (no string): " + id + "!");
}
var entry = jasy.Asset.resolve(id);
if (jasy.Env.isSet("debug"))
{
if (!entry) {
throw new Error("Could not figure out frame number of unknown image: " + id);
}
if (entry.t != "i") {
throw new Error("Could not figure out number of frames of non image asset: " + id);
}
}
return getFrameNumber(entry.d);
},
/**
* {Map} Returns the image data for the given asset @id {String} with the keys:
* `src`, `left`, `top`, `width` and `height` to being used in a sprite compatible
* image rendering mechanism (e.g. CSS background image + position, Canvas `dragImage`, etc.)
*/
getImage : function(id)
{
if (jasy.Env.isSet("debug")) {
core.Assert.isType(id, "String", "Invalid asset ID (no string): " + id + "!");
}
var entry = jasy.Asset.resolve(id);
if (jasy.Env.isSet("debug"))
{
if (!entry) {
throw new Error("Unknown image: " + id);
}
if (entry.t != "i") {
throw new Error("Could not figure out image data of non image asset: " + id);
}
}
var data = entry.d;
var width = data[0];
var height = data[1];
var spriteData = data[2];
if (spriteData)
{
var spriteId = jasy.Asset.resolveSprite(spriteData[0], id);
return {
node: getFromCache(spriteId, "node"),
src : jasy.Asset.toUri(spriteId),
left : spriteData[1],
top : spriteData[2],
width: width,
height: height
};
}
else
{
// Return compatible data format in cases where no sprite sheet is used
return {
node: getFromCache(id, "node"),
src : jasy.Asset.entryToUri(entry, id),
left : 0,
top: 0,
width: width,
height: height
};
}
},
/**
* {Map} Collects and returns data about the given image @id {String} on the given @frame {Number}.
* The following fields are available `src`, `left`, `top`, `width`, `height`, `offsetLeft`, `offsetTop` and `rotation`.
*/
getFrame : function(id, frame)
{
if (jasy.Env.isSet("debug"))
{
core.Assert.isType(id, "String", "Invalid asset ID (no string): " + id + "!");
core.Assert.isType(frame, "Integer", "Invalid frame number (no integer): " + frame + " for asset " + id + "!");
}
var entry = jasy.Asset.resolve(id);
if (jasy.Env.isSet("debug"))
{
if (!entry) {
throw new Error("Unknown image: " + id);
}
if (entry.t != "i") {
throw new Error("Could not figure out image data of non image asset: " + id);
}
}
var data = entry.d;
var width = data[0];
var height = data[1];
var spriteData = data[2];
if (spriteData)
{
var spriteId = jasy.Asset.resolveSprite(spriteData[0], id);
var src = jasy.Asset.toUri(spriteId);
var node = getFromCache(spriteId, "node");
var left = spriteData[1];
var top = spriteData[2];
}
else
{
var src = jasy.Asset.entryToUri(entry, id);
var node = getFromCache(id, "node");
var left = 0;
var top = 0;
}
var offsetLeft = 0;
var offsetTop = 0;
var rotation = 0;
// Detect whether a frame config is available
var frameData = data[3];
if (frameData)
{
var number = getFrameNumber(entry.d);
if (frame >= number && jasy.Env.isSet("debug")) {
throw new Error("Invalid frame number " + frame + " for asset " + id + "!");
}
// Manual frames
// Just one sub array with all frames configured manually
if (frameData.length == 1)
{
var frameConfig = frameData[0][frame];
// Format: left, top, width, height, offsetX?, offsetY?, rotation?
// Required fields
left += frameConfig[0];
top += frameConfig[1];
width = frameConfig[2];
height = frameConfig[3];
// Optional fields
if (frameConfig.length > 4)
{
offsetLeft = frameConfig[4] || 0;
offsetTop = frameConfig[5] || 0;
rotation = frameConfig[6] || 0;
}
}
// Automatic frames
else
{
// Correctly work when using sprite images
var cols = frameData[0];
var rows = frameData[1];
// Correct image dimensions
width /= cols;
height /= rows;
// Calculate position inside sprite image
left += (frame % cols) * width;
top += (~~(frame / cols)) * height; // jshint ignore:line
}
}
else if (frame !== 0 && jasy.Env.isSet("debug"))
{
throw new Error("Invalid frame number " + frame + " for asset " + id + "!");
}
var result =
{
node : node,
src : src,
left : left,
top : top,
width : width,
height : height,
offsetLeft : offsetLeft,
offsetTop : offsetTop,
rotation : rotation
};
// Prevent changes in object
if (jasy.Env.isSet("debug") && Object.freeze) {
Object.freeze(result);
}
return result;
}
});
})(core.Main.getGlobal(), Object);