使用JS / CSS实现更平滑的图像动画的技术

时间:2011-11-05 12:59:21

标签: javascript css image

我正在使用以下代码在网页的顶层上滑动图像,但它有点紧张,在图像上留下条纹垂直线,特别是当有多个嵌套元素的内容时。即使边框设置为零,也是如此。有关使用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);
}

7 个答案:

答案 0 :(得分:9)

我没有解决方案,我确信会阻止垂直线出现 但是我有一些提示来改进你的代码,以便提高性能,你可能会有线条消失。

  1. 将图像元素缓存到移动函数之外:

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

    在您的代码中,没有理由每3毫秒对DOM查询一次图像ID。 jQuery的选择器引擎,Sizzle需要做很多工作¹。

  2. 不要使用jQuery CSS函数:

    image.style.left = pos_new;

    设置属性对象比函数调用更快。对于jQuery css函数,至少有两个函数调用(一个到css,一个在css内。)

  3. 使用interval而不是timeout:

    setInterval(move, refresh);

      

    我会考虑一个我想成为的一次性动画的间隔   尽可能顺利

    setTimeout or setInterval?


  4. 更平滑动画的另一个选项是使用CSS过渡或动画。 John Resig在CSS Animations and JavaScript中可以找到一个很好的介绍和比较

    浏览器支持表:http://caniuse.com/#search=transition

    我发现一个JavaScript库,通过JavaScript很容易实现CSS动画morpheus


    ¹在引擎盖下,这是每3毫秒查找一次图像的代码:

    在支持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);
    };
    

    并且浏览器没有:

    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;
    };
    

答案 1 :(得分:4)

有许多小方法可以调整代码以使运行稍微平滑...使用反馈循环来优化步长和延迟,查找不会向上或向下舍入的步骤,导致定期跳转小跳等等。

但是您可能正在寻找的秘密API(以及您正在避免的许多库使用的秘密API)是requestAnimationFrame。它目前是非标准化的,因此每个浏览器都有一个前缀实现(webkitRequestAnimationFrame,mozRequestAnimationFrom等)

我不会重新解释它如何帮助减少/防止撕裂和vsync问题,而是指向文章本身:

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

答案 2 :(得分:3)

我在考虑一些想法的情况下对此进行了拍摄。我永远不会让动画变得非常不平滑,我也没有经历任何垂直线条,所以我不确定它是否甚至是一种改进。然而,下面的函数考虑了一些对我有意义的关键想法:

  • 使用容器<div>保持元素远离DOM,以用于动画。重绘的DOM参与使得它比基本的叠加动画要长得多。

  • 尽可能多地保留move功能。看到这个函数将被大量调用,运行的脚本越少越好。这包括用于更改元素位置的jQuery调用。

  • 只刷新绝对必要的内容。我将刷新间隔设置为121 Hz,但这对于60Hz显示器来说是绝对的高端。我可能建议61或更少,具体取决于需要。

  • 如果需要,只在元素样式对象中设置一个值。问题中的函数确实这样做了,但再次记住这是一件好事,因为在某些引擎中,只需访问样式对象中的setter就会强制重新绘制。

  • 我想要尝试的是使用图像作为元素的背景,因此您可以编写脚本来更改CSS background-position属性而不是更改元素位置。如果可能的话,这将意味着动画触发重绘中的DOM参与损失。

这个函数,对于你的测试,有一个相当不必要的闭包:

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);
        })();

    };

答案 3 :(得分:3)

根据您的情况,您应该考虑以下因素以使动画更流畅:

  1. 动画步骤之间的间隔(您的refresh值)应该足够长,以便浏览器处理(JavaScript代码,渲染)。根据我的经验,它应该是10到20毫秒。

  2. 为什么您将图像位置设为skip的倍数?将skip值设置得尽可能小(1)可以使动画更流畅。

  3. 尽可能避免导致浏览器重排(reflow vs repaint)。

  4. 使用适当的缓动方法而不是线性(如在代码中)可以使动画看起来更好(人类视觉,而不是技术)

  5. 优化每个动画步骤的JavaScript代码。这在你的简单动画中不是问题,但你可以改进一些东西,例如:使用setInterval而不是setTimeout,缓存图像对象以便快速访问,使用原生JS代码来改变图像位置

  6. 希望这些帮助。

答案 4 :(得分:3)

您的问题似乎与浏览器呈现引擎及其功能有关。正如您所注意到的,浏览器渲染动画的速度有限。如果达到此限制,您将看到抖动或其他“不平滑”行为。有时候会出现故障,比如没有清理部分动画或乱码部分 回到过去,任何形式的体面动画几乎都是不可能的。随着时间的推移,事情变得更好,但我仍然记得使用最微小的图像来保持我漂亮的折叠/展开菜单顺利进行。当然,现在我们已经有了硬件加速的浏览器渲染,所以你可以一次做多个动画,而且不需要担心动画很慢。
但我一直在重做一些我曾经使用过的动画,因为我的iPad(1)看起来很慢,而且渲染了一些。就像滚动一个大的div变得非常不稳定。所以基本上,我开始调整一下:

  • 使用简单动画而不是复杂动画,并且:没有组合动画(例如滚动淡化)
  • 减少动画对象内的html元素数量
  • 缩小动画对象
  • 尽可能预加载
  • 为动画对象创建空间(如果可能,滑动或移动意味着移动大量其他元素)

经过一些试验和错误,这确实有效。你需要记住的是,javascript只是改变了html-elements的css属性。浏览器重新绘制了JS告诉他的内容。所以它告诉他的越多,它越重,渲染就越落后 从性能上看,它分为三个部分:CPU,GPU和屏幕更新。每个浏览器引擎的工作方式都不同,因此性能也可能不同。一个有趣的看看这是如何工作的,来自IE 10团队的人,这比我更彻底:http://blogs.msdn.com/b/ie/archive/2011/04/26/understanding-differences-in-hardware-acceleration-through-paintball.aspx

答案 5 :(得分:3)

Javascript动画总是有点紧张,因为定时器不是很精确。你可以通过一些技巧获得更好的表现:

  • Enable hardware accelerationimg { -webkit-transform: translateZ(0) };
  • 使用setInterval,虽然更改通常不明显,但也可以使动画更流畅
  • 将刷新率设置为1000/60(60pfs) - 这是屏幕限制,计时器永远不会低于4毫秒

IE9 +似乎可以通过将滴答与屏幕刷新率相结合来解决这个问题,这样可以使动画更加流畅,但我不会指望其他浏览器能够很快做到这一点。未来是CSS过渡。

在CSS中,你可以使用它:

img {
  -webkit-transition:2s all linear;
     -moz-transition:2s all linear;
      -ms-transition:2s all linear;
          transition:2s all linear;
}

但是由于你的动画持续时间取决于目标位置以达到恒定速度,你可以通过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'

(为了简洁省略了一些跨浏览器的代码路径 - onload,forEach,Object.keys)

答案 6 :(得分:0)

尝试利用css变换和requestanimationframe功能。

请参阅TweenLite库:

http://www.greensock.com/v12/