Search code examples
cssanimationbrowsersubpixel

Subpixel issues with an animated sprite in odd zoom levels of Safari and FF


I have the following fiddle which distills an issue I am having with a larger project

http://jsfiddle.net/zhaocnus/6N3v8/

in Firefox and Safari, this animation will start having a jittering effect left and right on odd zoom levels (zoom in/out using Ctrl+/- or Cmd+/- on Mac). I believe this is do to sub-pixel rendering issues and the differences between the various browsers round up or down pixels during the zoom calculations, but I have no idea how to fix it and am looking for any suggestions.

I can't use more modern CSS3 animation features as I need to support legacy browsers like IE7.

(code from fiddle below, can't seem to post without it, although not sure it makes sense without CSS and HTML)

// js spritemap animation
// constants
var COUNTER_MAX = 9,
    OFFSET = -50,
    FRAMERATE = 100;

// variables
var _counter = 0,
    _animElm = document.getElementById('animation'),
    _supportBgPosX = false;

// functions    
function play() {
    // update counter
    _counter++;
    if (_counter > COUNTER_MAX) {
        _counter = 0;
    }

    // show current frame
    if (_supportBgPosX) {
        _animElm.style.backgroundPositionX = (_counter * OFFSET) + 'px';
    } else {
        _animElm.style.backgroundPosition = (_counter * OFFSET) + 'px 0';
    }

    // next frame    
    setTimeout(play, FRAMERATE);
}

// check if browser support backgroundPositionX
if (_animElm.style.backgroundPositionX != undefined) {
    _supportBgPosX = true;
}

// start animation
play();

Solution

  • Instead of moving the background to the new frame, have a canvas tag re-draw the frame. The canvas tag handles sub-pixel interpretation independent of the browser and because of this you not only control the render (browser agnostic) but also solve the jitter issue as it's being re-drawn frame-by-frame into the canvas' dimensions in realtime.

    Zooming is specifically tough because there's no reliable way to detect the zoom level from the browser using jQuery or plain-ole javascript.

    Check out the demo here: http://jsfiddle.net/zhaocnus/Gr9TF/

    *Credit goes to my coworker zhaocnus for the solution. I'm simply answering this question on his behalf.

    // js spritemap animation
    // constants
    var COUNTER_MAX = 14,
        OFFSET = -200,
        FRAMERATE = 100;
    
    // variables
    var _counter = 0,
        _sprite = document.getElementById("sprite"),
        _canvas = document.getElementById("anim-canvas"),
        _ctx = _canvas.getContext("2d"),
        _img = null;
    
    // functions    
    function play() {
        // update counter
        _counter++;
        if (_counter > COUNTER_MAX) {
            _counter = 0;
        }
    
        // show current frame
        _ctx.clearRect(0, 0, _canvas.width, _canvas.height);
        _ctx.drawImage(_img, _counter * OFFSET, 0);
    
        // next frame    
        setTimeout(play, FRAMERATE);
    }
    
    function getStyle(oElm, strCssRule) {
        var strValue = '';
        if (document.defaultView && document.defaultView.getComputedStyle) {
            strValue = document.defaultView.getComputedStyle(oElm, null).getPropertyValue(strCssRule);
        } else if (oElm.currentStyle) {
            var strCssRule = strCssRule.replace(_styleRegExp, function (strMatch, p1) {
                return p1.toUpperCase();
            });
            strValue = oElm.currentStyle[strCssRule];
        }
        return String(strValue);
    }
    
    function initCanvas(callback) {
        var url = getStyle(_sprite, 'background-image');
            url = url.replace(/^url\(["']?/, '').replace(/["']?\)$/, '');
        _img = new Image();
        _img.onload = function(){
            _ctx.drawImage(_img, 0, 0); 
            callback();
        };
        _img.src = url;
    }
    
    // start animation
    initCanvas(play);