Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Techniques for smoother image animation with JS/CSS

I am using the following code to glide an image across the top layer of a webpage but its a little jittery, giving streaky vertical lines down the image especially when over content with many nested elements. This is the case even when the border is set to zero. Any suggestions for a smoother method for gliding an image with JS/CSS?

border=4;
pps=250;  // speed of glide (pixels per second)
skip=2;  // e.g. if set to 10 will skip 9 in 10 pixels
refresh=3;  // how often looks to see if move needed in milliseconds

elem = document.createElement("img");
elem.id = 'img_id';
elem.style.zIndex="2000";
elem.style.position="fixed";
elem.style.top=0;
elem.style.left=0;
elem.src='http://farm7.static.flickr.com/6095/6301314495_69e6d9eb5c_m.jpg';
elem.style.border=border+'px solid black';
elem.style.cursor='pointer';
document.body.insertBefore(elem,null);

pos_start = -250;
pos_current = pos_start;
pos_finish = 20000;

var timer = new Date().getTime();
move();

function move ()
{
  var elapsed = new Date().getTime() - timer;
  var pos_new = Math.floor((pos_start+pps*elapsed/1000)/skip)*skip;

  if (pos_new != pos_current)
  {
    if (pos_new>pos_finish)
      pos_new=pos_finish;

    $("#img_id").css('left', pos_new);
    if (pos_new==pos_finish)
      return;

    pos_current = pos_new;
  }

  t = setTimeout("move()", refresh);
}
like image 758
flea whale Avatar asked Nov 05 '11 12:11

flea whale


3 Answers

I do not have a solution that I am sure of will prevent the vertical lines from appearing.
I do however have a couple of tips to improve your code so performance increases and you might have a chance that the lines disappear.

  1. Cache the image element outside of your move function:

    var image = $("#img_id")[0];

    In your code, there is no reason to query the image ID against the DOM every 3 milliseconds. jQuery's selector engine, Sizzle has to a lot of work¹.

  2. Don't use the jQuery CSS function:

    image.style.left = pos_new;

    Setting a property object is faster than a function call. In the case of the jQuery css function, there are at least two function calls (one to css and one inside css).

  3. Use interval instead of timeout:

    setInterval(move, refresh);

    I would consider an interval for one-off animations I wanted to be as smooth as possible

    setTimeout or setInterval?


One other option for smoother animation is to use CSS transitions or animations. A great introduction and comparison can be found in CSS Animations and JavaScript by John Resig

Browser support table: http://caniuse.com/#search=transition

A JavaScript library that I find makes CSS animation via JavaScript very easy is morpheus.


¹ Under the hood, this is the code it goes through every 3 milliseconds to find your image:

In a browser that supports querySelectorAll:

Sizzle = function( query, context, extra, seed ) {
    context = context || document;

    // Only use querySelectorAll on non-XML documents
    // (ID selectors don't work in non-HTML documents)
    if ( !seed && !Sizzle.isXML(context) ) {
        // See if we find a selector to speed up
        var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query );

        if ( match && (context.nodeType === 1 || context.nodeType === 9) ) {
            // Speed-up: Sizzle("TAG")
            if ( match[1] ) {
                return makeArray( context.getElementsByTagName( query ), extra );

            // Speed-up: Sizzle(".CLASS")
            } else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) {
                return makeArray( context.getElementsByClassName( match[2] ), extra );
            }
        }

        if ( context.nodeType === 9 ) {
            // Speed-up: Sizzle("body")
            // The body element only exists once, optimize finding it
            if ( query === "body" && context.body ) {
                return makeArray( [ context.body ], extra );

            // Speed-up: Sizzle("#ID")
            } else if ( match && match[3] ) {
                var elem = context.getElementById( match[3] );

                // Check parentNode to catch when Blackberry 4.6 returns
                // nodes that are no longer in the document #6963
                if ( elem && elem.parentNode ) {
                    // Handle the case where IE and Opera return items
                    // by name instead of ID
                    if ( elem.id === match[3] ) {
                        return makeArray( [ elem ], extra );
                    }

                } else {
                    return makeArray( [], extra );
                }
            }

            try {
                return makeArray( context.querySelectorAll(query), extra );
            } catch(qsaError) {}

        // qSA works strangely on Element-rooted queries
        // We can work around this by specifying an extra ID on the root
        // and working up from there (Thanks to Andrew Dupont for the technique)
        // IE 8 doesn't work on object elements
        } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) {
            var oldContext = context,
                old = context.getAttribute( "id" ),
                nid = old || id,
                hasParent = context.parentNode,
                relativeHierarchySelector = /^\s*[+~]/.test( query );

            if ( !old ) {
                context.setAttribute( "id", nid );
            } else {
                nid = nid.replace( /'/g, "\\$&" );
            }
            if ( relativeHierarchySelector && hasParent ) {
                context = context.parentNode;
            }

            try {
                if ( !relativeHierarchySelector || hasParent ) {
                    return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra );
                }

            } catch(pseudoError) {
            } finally {
                if ( !old ) {
                    oldContext.removeAttribute( "id" );
                }
            }
        }
    }

    return oldSizzle(query, context, extra, seed);
};

And a browser that doesn't:

var Sizzle = function( selector, context, results, seed ) {
    results = results || [];
    context = context || document;

    var origContext = context;

    if ( context.nodeType !== 1 && context.nodeType !== 9 ) {
        return [];
    }

    if ( !selector || typeof selector !== "string" ) {
        return results;
    }

    var m, set, checkSet, extra, ret, cur, pop, i,
        prune = true,
        contextXML = Sizzle.isXML( context ),
        parts = [],
        soFar = selector;

    // Reset the position of the chunker regexp (start from head)
    do {
        chunker.exec( "" );
        m = chunker.exec( soFar );

        if ( m ) {
            soFar = m[3];

            parts.push( m[1] );

            if ( m[2] ) {
                extra = m[3];
                break;
            }
        }
    } while ( m );

    if ( parts.length > 1 && origPOS.exec( selector ) ) {

        if ( parts.length === 2 && Expr.relative[ parts[0] ] ) {
            set = posProcess( parts[0] + parts[1], context, seed );

        } else {
            set = Expr.relative[ parts[0] ] ?
                [ context ] :
                Sizzle( parts.shift(), context );

            while ( parts.length ) {
                selector = parts.shift();

                if ( Expr.relative[ selector ] ) {
                    selector += parts.shift();
                }

                set = posProcess( selector, set, seed );
            }
        }

    } else {
        // Take a shortcut and set the context if the root selector is an ID
        // (but not if it'll be faster if the inner selector is an ID)
        if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML &&
                Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) {

            ret = Sizzle.find( parts.shift(), context, contextXML );
            context = ret.expr ?
                Sizzle.filter( ret.expr, ret.set )[0] :
                ret.set[0];
        }

        if ( context ) {
            ret = seed ?
                { expr: parts.pop(), set: makeArray(seed) } :
                Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML );

            set = ret.expr ?
                Sizzle.filter( ret.expr, ret.set ) :
                ret.set;

            if ( parts.length > 0 ) {
                checkSet = makeArray( set );

            } else {
                prune = false;
            }

            while ( parts.length ) {
                cur = parts.pop();
                pop = cur;

                if ( !Expr.relative[ cur ] ) {
                    cur = "";
                } else {
                    pop = parts.pop();
                }

                if ( pop == null ) {
                    pop = context;
                }

                Expr.relative[ cur ]( checkSet, pop, contextXML );
            }

        } else {
            checkSet = parts = [];
        }
    }

    if ( !checkSet ) {
        checkSet = set;
    }

    if ( !checkSet ) {
        Sizzle.error( cur || selector );
    }

    if ( toString.call(checkSet) === "[object Array]" ) {
        if ( !prune ) {
            results.push.apply( results, checkSet );

        } else if ( context && context.nodeType === 1 ) {
            for ( i = 0; checkSet[i] != null; i++ ) {
                if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) {
                    results.push( set[i] );
                }
            }

        } else {
            for ( i = 0; checkSet[i] != null; i++ ) {
                if ( checkSet[i] && checkSet[i].nodeType === 1 ) {
                    results.push( set[i] );
                }
            }
        }

    } else {
        makeArray( checkSet, results );
    }

    if ( extra ) {
        Sizzle( extra, origContext, results, seed );
        Sizzle.uniqueSort( results );
    }

    return results;
};
like image 186
DADU Avatar answered Nov 15 '22 15:11

DADU


There are lots of minor ways to tweak you code to run slightly smoother... Use a feedback loop to optimize the step size and delay, look for even steps that don't round up or down causing small jumps at regular intervals, etc.

But the secret API you're probably looking for (and which is used by many of the libraries you are avoiding) is requestAnimationFrame. It's currently non-standarized, so each browser has a prefixed implementation (webkitRequestAnimationFrame, mozRequestAnimationFrom, etc.)

Instead of re-explaining how it helps reduce/prevent tearing and vsync issues, I'll point you to the article itself:

http://robert.ocallahan.org/2010/08/mozrequestanimationframe_14.html

like image 28
Lilith River Avatar answered Nov 15 '22 16:11

Lilith River


I took a shot at this with a few ideas in mind. I could never get the animation to be incredibly un-smooth, nor did I ever experience any vertical lines, so I'm not sure if it's even an improvement. Nevertheless, the function below takes a few key ideas into account that make sense to me:

  • Keep the element away from the DOM with a container <div> for the animation. DOM involvement in repaints makes it much longer than it should be for a basic overlay animation.

  • Keep as much fat as possible out of the move function. Seeing as this function will be called a large amount, the less script there is to run, the better. This includes that jQuery call to change the element position.

  • Only refresh as much as absolutely necessary. I set the refresh interval here to 121 Hz, but that's an absolute top-end for a 60Hz monitor. I might suggest 61 or less, depending on what's needed.

  • Only set a value in to the element style object if it's needed. The function in the question did do this, but again it's a good thing to keep in mind, because in some engines simply accessing the setter in a style object will force a repaint.

  • What I wanted to try out was using the image as the background of an element, so you could just script changing the CSS background-position property instead of changing the element position. This would mean loss DOM involvement in the repaints triggered by the animation, if possible.

And the function, for your testing, with a fairly unnecessary closure:

var border = 4;
var pps = 250;
var skip = 2;
var refresh = 1000 / 121; // 2 * refresh rate + 1
var image = new Image();
image.src = 'http://farm7.static.flickr.com/6095/6301314495_69e6d9eb5c_m.jpg';
// Move img (Image()) from x1,y1 to x2,y2
var moveImage = function (img, x1, y1, x2, y2) {
        x_min = (x1 > x2) ? x2 : x1;
        y_min = (y1 > y2) ? y2 : y1;
        x_max = (x1 > x2) ? x1 : x2;
        y_max = (y1 > y2) ? y1 : y2;
        var div = document.createElement('div');
        div.id = 'animationDiv';
        div.style.zIndex = '2000';
        div.style.position = 'fixed';
        div.style.top = y_min;
        div.style.left = x_min;
        div.style.width = x_max + img.width + 'px';
        div.style.height = y_max + img.height + 'px';
        div.style.background = 'none';
        document.body.insertBefore(div, null);
        elem = document.createElement('img');
        elem.id = 'img_id';
        elem.style.position = 'relative';
        elem.style.top = 0;
        elem.style.left = 0;
        elem.src = img.src;
        elem.style.border = border + 'px solid black';
        elem.style.cursor = 'pointer';

        var theta = Math.atan2((y2 - y1), (x2 - x1));
        (function () {
            div.insertBefore(elem, null);
            var stop = function () {
                    clearInterval(interval);
                    elem.style.left = x2 - x1;
                    elem.style.top = y2 - y1;
                };
            var startTime = +new Date().getTime();
            var xpmsa = pps * Math.cos(theta) / (1000 * skip); // per milli adjusted
            var ypmsa = pps * Math.sin(theta) / (1000 * skip);

            var interval = setInterval(function () {
                var t = +new Date().getTime() - startTime;
                var x = (Math.floor(t * xpmsa) * skip);
                var y = (Math.floor(t * ypmsa) * skip);
                if (parseInt(elem.style.left) === x &&
                    parseInt(elem.style.top) === y) return;
                elem.style.left = x + 'px';
                elem.style.top = y + 'px';
                if (x > x_max || x < x_min || y > y_max || y < y_min) stop();
            }, refresh);
            console.log(xpmsa, ypmsa, elem, div, interval);
        })();

    };
like image 24
MischaNix Avatar answered Nov 15 '22 16:11

MischaNix