Методы для более плавной анимации изображений с помощью JS/CSS

Я использую следующий код для скольжения изображения по верхнему слою веб-страницы, но его немного неудобно, что дает строгие вертикальные линии вниз по изображению, особенно когда над содержимым много вложенных элементов. Это имеет место, даже если граница установлена ​​равной нулю. Любые предложения по более плавному методу скольжения изображения с помощью 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);
}

Ответ 1

У меня нет решения, которое, я уверен, предотвратит появление вертикальных линий.
Однако у меня есть несколько советов по улучшению кода, поэтому производительность увеличивается, и у вас может быть шанс, что линии исчезнут.

  • Загрузите элемент изображения вне функции перемещения:

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

    В вашем коде нет причин запрашивать идентификатор изображения в DOM каждые 3 миллисекунды. Механизм селектора jQuery, Sizzle должен много работать¹.

  • Не используйте функцию jQuery CSS:

    image.style.left = pos_new;

    Установка объекта свойства выполняется быстрее, чем вызов функции. В случае функции jQuery css существует как минимум два вызова функций (один на css и один внутри css).

  • Используйте интервал вместо таймаута:

    setInterval(move, refresh);

    Я бы рассмотрел интервал для одноразовых анимаций, которым я хотел быть как как можно лучше

    setTimeout или setInterval?


Другим вариантом для более плавной анимации является использование переходов или анимаций CSS. Отличное введение и сравнение можно найти в CSS-анимации и JavaScript от Джона Резига

Таблица поддержки браузера: http://caniuse.com/#search=transition

Библиотека JavaScript, которую я нахожу, очень упрощает CSS-анимацию через JavaScript, 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;
};

Ответ 2

Есть много мелких способов настроить ваш код, чтобы он работал немного плавно... Используйте цикл обратной связи для оптимизации размера шага и задержки, ищите четные шаги, которые не округляются вверх или вниз, что вызывает небольшие прыжки через равные промежутки времени, и т.д.

Но секретный API, который вы, вероятно, ищете (и который используется многими из библиотек, которого вы избегаете), является requestAnimationFrame. В настоящее время он не является стандартизированным, поэтому каждый браузер имеет префиксную реализацию (webkitRequestAnimationFrame, mozRequestAnimationFrom и т.д.).

Вместо повторного объяснения того, как это помогает уменьшить/предотвратить проблемы с разрывом и vsync, я укажу вам на статью:

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

Ответ 3

Я сделал это с несколькими идеями. Я никогда не мог заставить анимацию быть невероятно негладкой, и я никогда не испытывал никаких вертикальных линий, поэтому я не уверен, что это даже улучшение. Тем не менее, нижеприведенная функция учитывает несколько ключевых идей, которые имеют смысл для меня:

  • Держите элемент подальше от DOM с контейнером <div> для анимации. Участие DOM в repaints делает его намного дольше, чем должно быть для базовой анимации оверлей.

  • Храните как можно больше жира из move. Увидев, что эта функция будет называться большой суммой, тем меньше script нужно запустить, тем лучше. Это включает в себя вызов jQuery для изменения позиции элемента.

  • Обновляйте столько, сколько необходимо. Я установил интервал обновления здесь на 121 Гц, но это абсолютный верхний конец для монитора с частотой 60 Гц. Я мог бы предложить 61 или меньше, в зависимости от того, что нужно.

  • Установите значение в объект стиля элемента, если это необходимо. Функция в вопросе действительно делала это, но опять-таки хорошо, что нужно помнить, потому что в некоторых машинах просто доступ к установщику в объекте стиля заставит перерисовать.

  • То, что я хотел попробовать, - использовать изображение в качестве фона элемента, поэтому вы можете просто изменить свойство 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);
        })();

    };

Ответ 4

Для ваших обстоятельств вы должны учитывать следующее, чтобы сделать анимацию более гладкой:

  • Интервал между шагами анимации (ваше значение refresh) должен быть достаточно длинным для обработки браузера (код JavaScript, рендеринг). По моему опыту, это должно быть от 10 до 20 миллисекунд.

  • Почему вы сделали положение изображения кратным skip? Установите значение skip как можно меньше (1), чтобы сделать анимацию более плавной.

  • Избегайте возможного перепланирования браузеров (reflow vs repaint).

  • Использование соответствующего метода ослабления вместо линейного (как в вашем коде) может сделать анимацию лучше (человеческое зрение, а не техническое)

  • Оптимизируйте код JavaScript для каждого шага анимации. Это не проблема в простой анимации, как ваша, но вы можете улучшить что-то вроде: используйте setInterval вместо setTimeout, объект изображения с кешем для быстрого доступа, используйте собственный JS-код для изменения положения изображения

Надеюсь на эту помощь.

Ответ 5

Ваш вопрос действительно выглядит о механизмах рендеринга браузеров и их возможностях. Как вы заметили, существуют ограничения относительно того, насколько быстро браузер может отображать анимацию. Если вы нажмете это ограничение, вы увидите джиттер или другое "негласное" поведение. Иногда возникают ошибки, например, не очистка частей анимации или скремблированных частей.
Еще в прежние времена любая форма приличной анимации была практически невозможна. Со временем все стало лучше, но я все еще помню, используя самые мелкие возможные изображения, чтобы мое хорошее меню складывания/разворачивания выполнялось плавно. Конечно, в наши дни у нас есть аппаратное ускоренное отображение браузера, поэтому вы можете делать сразу несколько анимаций, и вам не нужно много беспокоиться о медленной анимации.
Но я переработал некоторые анимации, которые я использовал, потому что мой iPad (1) кажется довольно медленным, когда некоторые из них. Подобно прокрутке большой div получился довольно изменчивый. Поэтому в основном я начал настраивать вещи:

  • Использование простой анимации вместо сложной и: нет комбинированной анимации (например, прокрутки и стирания)
  • Уменьшить количество html-элементов внутри анимированного объекта
  • Уменьшить размер анимированного объекта
  • Предварительно загрузить как можно больше
  • Создайте пространство для анимированного объекта (если возможно, в случае, если скользящие или движущиеся средства перемещают множество других элементов)

Это работало после некоторых проб и ошибок. Что вы должны иметь в виду, так это то, что javascript просто меняет свойства css html-элементов. Браузер переписывает то, что ему говорит JS. Таким образом, чем больше он говорит ему, тем тяжелее он получает, а рендеринг отстает.
Глядя на производительность, он разбивается на три компонента: CPU, GPU и обновления экрана. Каждый движок браузера работает по-разному, поэтому производительность может отличаться. Интересный взгляд на то, как это работает, исходит от людей в команде IE 10, которая является более тщательной, чем я мог бы быть: http://blogs.msdn.com/b/ie/archive/2011/04/26/understanding-differences-in-hardware-acceleration-through-paintball.aspx

Ответ 6

Javascript анимации всегда несколько нервничают, поскольку таймеры не очень точны. Вы можете получить немного лучше, используя несколько трюков:

  • Включить аппаратное ускорение: img { -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)+ 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)

Ответ 7

Попробуйте воспользоваться функциями css transforms и requestanimationframe.

См. библиотеку TweenLite:

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