I'm working on a texture picker intended for use on iPad. So basically just a bunch of image elements. To avoid image reloading and lag, I cache and reuse the Image
objects in JS. Sort of this
/**
* Asynchronous version of memoize for use with callback functions. Asserts
* that last argument is the callback.
*
* @param {Function} func
* @return {Function}
*/
util.memoize.async = function(func) {
var cache = {};
return function() {
var hash = JSON.stringify(arguments);
var args = Array.prototype.splice.call(arguments, 0);
var callback = args.pop();
if (hash in cache) {
return callback.apply(this, cache[hash]);
}
args.push(function() {
cache[hash] = Array.prototype.splice.call(arguments, 0);
callback.apply(this, cache[hash]);
});
return func.apply(this, args);
};
};
/**
* Creates new Image element and calls back with loaded image.
* @param {string} url
*/
io.GetImage = function(url, callback) {
var img = new Image();
img.onload = function() {
callback(img);
};
img.src = url;
};
picker.image_ = util.memoize.async(io.GetImage);
Then whenever I need the image, I call picker.image_
and get the cached one. It works flawlessly on the desktop, Chrome, Firefox, Safari, but on the iPad, I'm getting empty (not loaded) image back. Why is that? I really like this approach, it performs really well.
It looks like as if Mobile Safari drops the image data when it's removed from DOM. Could that be?
UPDATE: To clarify, the data being loaded is dynamic, therefore it's not the fittest use case for AppCache.
UPDATE*: There was not fully satisfying answer, here's my solution. Note that copy method is quite slow.
/**
* Creates new Image element and calls back with loaded image.
* @param {string} url
*/
var GetImage = function(url, callback) {
var img = new Image();
img.onload = function() {
callback(img);
};
img.src = url;
};
/**
* @param {number} num maximum number of stored images
*/
var ImagePool = function(num) {
this.limit_ = num;
this.canvases_ = {};
this.order_ = [];
};
/**
* Retrieve image from cache.
*
* @param {string} url URL of request image
* @param {function(HTMLCanvasElement)} callback
*/
ImagePool.prototype.get = function(url, callback) {
if (this.canvases_[url] !== undefined) {
callback(this.copy_(url));
} else {
if (this.limit_ && this.order_.length == this.limit_) {
delete this.canvases_[url];
this.order_.pop();
}
GetImage(realUrl, function(img) {
var c = document.createElement('canvas');
c.width = img.width;
c.height = img.height;
var ctx = c.getContext('2d');
ctx.drawImage(img, 0, 0);
this.canvases_[url] = c;
this.order_.unshift(url);
callback(this.copy_(url));
}.bind(this));
}
};
/**
* @param {string} url
* @return {HTMLCanvasElement}
* @private
*/
ImagePool.prototype.copy_ = function(url) {
var c = document.createElement('canvas'),
cached = this.canvases_[url];
c.width = cached.width;
c.height = cached.height;
var ctx = c.getContext('2d');
ctx.drawImage(cached, 0, 0);
return c;
};
I think your problem could be best solved by using an HTML 5 offline application cache. You list your resources that you would like cached, and after users visit a page that requests those resources, they are cached for later use. You would still have to have your UI wait until your images are loaded, but once they have been loaded, you won't have to worry about them being dropped simply because they're not in the DOM (This SO question suggests that images that are in the DOM, but not displayed on screen, are dropped as well).
Apparently, Safari Mobile has a 5MB cache limit, which can be increased by asking users to agree to expand it (Source). A comment in this blog post suggests that this expansion prompt is available as soon as iOS 4.
Helpful links:
Apple Developer Guide for HTML 5 Storage - Some documentation on using HTML 5 offline application storage in Safari.
Blog post outlining iPhone problems of a similar nature - It's an older blog post (2009-2010), but the author has some good advice for using HTML 5 features (under the heading "HTML5 offline application cache").
if you attach the Image object to the DOM directly you might lose it on remove, you could try cloning it before deleting or attaching
function clone(obj) {
if (null == obj || "object" != typeof obj) return obj;
var copy = obj.constructor();
for (var attr in obj) {
if (obj.hasOwnProperty(attr)) copy[attr] = obj[attr];
}
return copy;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With