Предотвратить постоянный запуск requestAnimationFrame

Я хотел бы знать, как вызывать функцию animate через requestAnimationFrame только тогда, когда это действительно необходимо. В настоящее время animate вызывается все время, что, я думаю, создает накладные расходы.

Я уже пытался в своей функции animate сравнить targetRadius и исходный radius и вернуть false, как только они совпадают. К сожалению, это вообще не работает.

Может кто-нибудь объяснить мне, как это решить?

jsfiddle

HTML:

  <canvas id="ddayCanvas" width="288" height="288" data-image="http://www.topdesignmag.com/wp-content/uploads/2011/07/64.png">
    <div>
        <div class="product-image"></div>
        <div class="product-box">...</div>
        <a href="#" class="overlay">...</a>
    </div>    
  </canvas>

JS:

// Options
var maxImageWidth = 250,
    maxImageHeight = 196;

var canvas = $('#ddayCanvas'),
    canvasWidth = canvas.width(),
    canvasHeight = canvas.height(),
    sectorColor = $('.product-box').css('background-color'),
    context = canvas[0].getContext('2d'),
    imageSrc = canvas.data('image'),
    imageObj = new Image(),
    imageWidth, imageHeight,
    mouseover = false;

    imageObj.onload = function() {
        imageWidth = this.width;
        imageHeight = this.height;

        if (imageWidth > maxImageWidth){
            imageHeight = imageHeight - (imageWidth - maxImageWidth);
            imageWidth = maxImageWidth;
        }

        if (imageHeight > maxImageHeight) {
            imageWidth = imageWidth - (imageHeight - maxImageHeight);
            imageHeight = maxImageHeight;
        }

        drawDday(90); 
    };

    imageObj.src = imageSrc;  

function drawDday (radius) {
    context.clearRect(0, 0, canvasWidth, canvasHeight);
    context.drawImage(imageObj, Math.ceil((canvasWidth - imageWidth) / 2), Math.ceil((canvasHeight - imageHeight) / 2), imageWidth, imageHeight);
    context.fillStyle = sectorColor;
    context.beginPath();
    context.rect(0, 0, canvasWidth, canvasHeight);
    context.arc(canvasWidth/2, canvasHeight/2, radius, 0, Math.PI*2, true);
    context.closePath();
    context.fill();

    // Check out the console
    console.log('test');
}


var radius = baseRadius = 90,
    targetRadius = 110,
    ease = 50,
    speed = 2;

function animate(){
    if(mouseover){
        radius += ((targetRadius-radius)/ease)*speed;
    } else {
        radius -= ((radius-baseRadius)/ease)*speed;
    }
    if(radius > targetRadius) radius = targetRadius;
    if(radius < baseRadius) radius = baseRadius;

    drawDday(radius);   
    requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

canvas.on('mouseover', function(e){
    mouseover = true;
}).on('mouseout', function(){
    mouseover = false;
});

person damian    schedule 18.12.2013    source источник
comment
Функция ослабления является асимптотической. Он может не достигать целевого значения в течение некоторого времени. Вы можете попробовать Math.floor(), Math.ceil() или Math.round(), чтобы убедиться, что он достигает целевого значения.   -  person Synthetx    schedule 18.12.2013
comment
Радиус - это десятичное число, и я использовал, как вы предложили, Math.ceil(), но это никак не влияет. Почему?   -  person damian    schedule 18.12.2013
comment
Это демонстрирует асимптотический характер функции легкости и то, почему ваш сравнительный тест, вероятно, не работает: jsfiddle.net/EHEuZ/ 1   -  person Synthetx    schedule 18.12.2013
comment
Я знаю об этом, но это совсем не решает проблему, или я что-то не так понял? Я думаю, проблема в том, что функция animate представляет собой бесконечный цикл, так как drawDday(radius); и requestAnimationFrame(animate); всегда вызываются. Но я не уверен, как сломать функцию, когда ее не нужно вызывать.   -  person damian    schedule 18.12.2013


Ответы (2)


Вам нужно реализовать условие, чтобы вы могли разорвать цикл, например (при необходимости принять):

var isRunning = true;

function loop() {

    ... funky stuff here ...

    /// test condition before looping
    if (isRunning) requestAnimationFrame(loop);
}

Теперь, когда вы установите isRunning на false, цикл прервется. Для удобства рекомендуется иметь метод для запуска и остановки цикла:

function startLoop(state) {

    if (state && !isRunning) {
        isRunning = true;
        loop();             /// starts loop

    } else if (!state && isRunning) {
        isRunning = false;
    }
}

Условие может быть установлено чем угодно, что вам нужно, например, при обратном вызове после завершения анимации и т. д. Важной частью является то, что флаг условия доступен для обеих областей, использующих его (т.е. чаще всего в глобальном объем).

ОБНОВЛЕНИЕ:

Более конкретно в этом случае то, что ваше условие (радиус) никогда не достигнет условия, необходимого для остановки цикла.

Вот что вы можете сделать, чтобы это исправить:

ДЕМО

var isPlaying = false;

function animate(){
    /**
     * To make sure you will reach the condition required you need
     * to either make sure you have a fall-out for the steps or the
     * step will become 0 not adding/subtracting anything so your
     * checks below won't trigger. Here we can use a simple max of
     * the step and a static value to make sure the value is always > 0
    */
    if(mouseover){
        radius += Math.max( ((targetRadius-radius)/ease)*speed, 0.5);
    } else {
        radius -= Math.max( ((radius-baseRadius)/ease)*speed,   0.5);
    }

    /**
     * Now the checks will trigger properly and we can use the
     * isPlaying flag to stop the loop when targets are reached.
    */
    if(radius >= targetRadius) {
        radius = targetRadius;
        isPlaying = false;              /// stop loop after this
    } else if (radius <= baseRadius) {
        radius = baseRadius;
        isPlaying = false;              /// stop loop after this
    }
    
    drawDday(radius);

    /// loop?
    if (isPlaying === true) requestAnimationFrame(animate);
}

Чтобы запустить цикл, мы используем метод, который проверяет, работает ли цикл, если нет, он сбрасывает флаг isPlaying и запускает цикл. Мы делаем это внутри обоих mouseover и mouseout:

canvas.on('mouseover', function(e){
    mouseover = true;
    startAnim();

}).on('mouseout', function(){
    mouseover = false;
    startAnim();
});

Метод просто проверяет isPlaying, и если он не установлен, он устанавливает значение true и запускает цикл — так что цикл запускается только один раз:

function startAnim() {
    if (!isPlaying) {
        isPlaying = true;
        requestAnimationFrame(animate);
    }
}

В демо я добавил ведение журнала консоли, чтобы показать, когда цикл запущен и когда цели поражены.

Надеюсь это поможет.

person Community    schedule 18.12.2013
comment
Я знаю об использовании условий, но в этом случае это почему-то не работает. Например. в настоящее время в глобальной области видимости есть requestAnimationFrame(animate);, но если я помещу это в событие mouseover, оно ничего не сделает. Не могли бы вы привести пример в этом особом случае? - person damian; 18.12.2013
comment
Большое спасибо за более подробный ответ! - person damian; 23.12.2013

Причина, по которой ваша функция animate вызывается непрерывно, заключается в том, что вы начинаете с вызова requestAnimationFrame(animate);, а затем каждый вызов animate безоговорочно вызывает requestAnimationFrame(animate); снова. Цикл никогда не будет прерван, если вы не используете cancelAnimationFrame в какой-то момент (чего вы не делаете) или убедитесь, что animate запрашивает другой кадр, только если это необходимо.

Другая проблема заключается в том, что radius в настоящее время никогда не достигнет ни targetRadius, ни baseRadius, и, следовательно, ни одно из следующих утверждений никогда не будет истинным:

if(radius > targetRadius) radius = targetRadius;
if(radius < baseRadius) radius = baseRadius;

Это напрямую не отвечает за непрерывные вызовы animate, но поскольку targetRadius и baseRadius используются для указания конечных точек вашей анимации, нам нужно сформировать с ними какое-то разумное условное выражение.

Итак, вы можете сделать что-то вроде: http://jsfiddle.net/PLDUq/9/

var radius = baseRadius = 50,
    targetRadius = 110,
    ease = 50,
    speed = 12,
    currentAnim;

function animate(){
    if(mouseover){
        radius += ((targetRadius-radius)/ease)*speed;
    } else {
        radius -= ((radius-baseRadius)/ease)*speed;
    }

    drawDday(radius);

    if(Math.round(radius) >= targetRadius) {
        // uses Math.round() to ensure condition can be fulfilled

        radius = targetRadius;
        return; // doesn't call next frame
    }
    if(Math.round(radius) <= baseRadius) {
        radius = baseRadius;
        return; // doesn't call next frame
    }

    requestAnimationFrame(animate);
}

canvas.on('mouseenter mouseleave', function (e) {
    if (currentAnim) {requestAnimationFrame(currentAnim);}
    // cancels current animation if one is playing to
    // prevent several concurrent loops calling animate()

    mouseover = (e.type === 'mouseenter');

    requestAnimationFrame(animate);
});
person Ben Jackson    schedule 21.12.2013