Reputation: 1803
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);
}
Upvotes: 10
Views: 4404
Reputation: 16037
Try taking advantage of css transforms and requestanimationframe feature.
See the TweenLite library:
Upvotes: 0
Reputation: 7086
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.
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¹.
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
).
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
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;
};
Upvotes: 9
Reputation: 35283
Javascript animations are always somewhat jittery, since timers aren't very precise. You can get a little better peformance by using a few tricks:
img { -webkit-transform: translateZ(0) };
setInterval
, it can result in smoother animation too, although the change is usually unnoticeable1000/60
(60pfs) - that's the screen limit, and timers never go below 4msIE9+ seems to solve this by coupling ticks with the screen refresh rate, which makes for much smoother animation, but I wouldn't count on other browsers doing that anytime soon. The future is in CSS transitions.
In CSS, you could use this:
img {
-webkit-transition:2s all linear;
-moz-transition:2s all linear;
-ms-transition:2s all linear;
transition:2s all linear;
}
But since your animation duration depends on the target position to achieve a constant speed, you can manipulate the values via JS:
var img = document.createElement('img')
document.body.appendChild(img)
var styles = {
zIndex : '2000'
, position : 'absolute'
, top : '0px'
, left : '0px'
, border : '4px solid black'
, cursor : 'pointer'
}
Object.keys(styles).forEach(function(key){
img.style[key] = styles[key]
})
var prefixes = ['webkit', 'Moz', 'O', 'ms', '']
, speed = 250
, endPosition = 2000
, transition = Math.floor(endPosition/speed)+'s all linear'
prefixes.forEach(function(prefix){
img.style[prefix+(prefix ? 'T' : 't')+'ransition'] = transition
})
img.onload = function(){
img.style.left = endPosition+'px' // starts the animation
}
img.src = 'http://farm7.static.flickr.com/6095/6301314495_69e6d9eb5c_m.jpg'
(left out a few cross-browser code paths for brevity - onload, forEach, Object.keys)
Upvotes: 3
Reputation: 1554
Your question really seems to be about browser rendering engines and their capabilities. As you have noticed, there are limitations as to how quick a browser can render animation. If you hit this limitation, you'll see jitter or other 'unsmooth' behavior. Sometimes rendering faults, like not cleaning up parts of the animation or scrambled parts.
Back in the olden days, any form of decent animation was virtually impossible. In time, things got better, but I still remember using the tiniest possible images to keep my nice folding/unfolding menu performing smoothly. Of course, these days we've got hardware accelerated browser rendering, so you can do multiple animations at once, and don't need to worry a whole lot about animation being slow.
But I've been redoing some animations I've used, because my iPad (1) seems quite slow rendering some of them. Like scrolling a large div
got quite choppy. So basically, I started to tune things down:
This did work, after some trial and error. What you've got to keep in mind is that the javascript is just changing the css properties of html-elements. The browser repaints what the JS tells him to. So the more it tells him, the heavier it gets, and the rendering falls behind.
Looking at performance, it breaks down into three components: CPU, GPU and screen updates. Every browser engine works differently, so performance can differ as well. An interesting look at how this works, comes from the people on the IE 10 team, which is more thorough than I could be: http://blogs.msdn.com/b/ie/archive/2011/04/26/understanding-differences-in-hardware-acceleration-through-paintball.aspx
Upvotes: 3
Reputation: 3926
For your circumstance, you should consider the followings to make animation smoother:
The interval between animation steps (your refresh
value) should be long enough for browser to process (JavaScript code, rendering). As my experience, it should be 10 to 20 milliseconds.
Why you made the image position multiple of skip
? Set skip
value as small as possible (1) could make animation smoother.
Avoid causing browsers reflow if possible (reflow vs repaint).
Using appropriate easing method instead of linear (as in your code) could make animation look better (human sight, not technical)
Optimize JavaScript code for each animation step. This is not problem in simple animation as yours, but you can improve something such as: use setInterval instead of setTimeout, cache image object for fast access, use native JS code to change image position
Hope these help.
Upvotes: 3
Reputation: 16468
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
Upvotes: 4
Reputation: 730
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);
})();
};
Upvotes: 3