4

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;
};
skrat
  • 5,518
  • 3
  • 32
  • 48
  • so why don't you keep it in DOM and make it invisible? – U.P Jun 22 '12 at 13:10
  • I'd have to go great lengths to do this within my UI system, definitely not an elegant solution (what I'm looking for). – skrat Jun 22 '12 at 13:24

4 Answers4

5

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:

Community
  • 1
  • 1
RustyTheBoyRobot
  • 5,891
  • 4
  • 36
  • 55
  • Perhaps I eventually get to applying application cache, this data is dynamic, I would have to constantly push updates to the manifest. Almost forgot this one http://www.alistapart.com/articles/application-cache-is-a-douchebag/ – skrat Jun 24 '12 at 22:19
  • @skrat - You most definitely need to edit your question to add the requirement of the images being dynamic data. That changes things quite a bit. – RustyTheBoyRobot Jun 24 '12 at 22:42
  • @skrat - And I'm just curious; why are your images going to be so dynamic? – RustyTheBoyRobot Jun 25 '12 at 21:54
  • Its a texture library for canvas drawing. User generated one. – skrat Jun 26 '12 at 09:28
3

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;
}
cuzzea
  • 1,515
  • 11
  • 22
  • This was tried, but this question boils down to 6 MB limit of Mob Safari. Be it in the DOM or not. – skrat Jun 29 '12 at 13:59
2

Have you tried just setting the appropriate headers at the server and letting the browser manage the caching instead of using this scheme?

Tom
  • 7,994
  • 8
  • 45
  • 62
  • I am indeed hitting Mobile Safari limitations, so far my workaround is to maintain image pool, render every downloaded image onto canvas, and copy it when needed. – skrat Jun 24 '12 at 22:17
1

I would use embedded image src base64 encoded. Just check here

So you can store your data as string as JSON if you want and get it back when ever you want you can also save it to localStorage if you are calling images from a different URL.

And yes it cause you a huge html content and storage space but in your case it definitely worth it.

Onur Topal
  • 3,042
  • 1
  • 24
  • 41