Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Image caching with JS on iPad

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;
};
like image 235
skrat Avatar asked Dec 20 '22 20:12

skrat


2 Answers

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").

like image 58
RustyTheBoyRobot Avatar answered Dec 23 '22 09:12

RustyTheBoyRobot


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;
}
like image 22
cuzzea Avatar answered Dec 23 '22 09:12

cuzzea