JavaScriptové animace

Animace JavaScriptu zvládnou věci, které CSS nezvládnou.

Například pohyb po složité cestě s funkcí časování odlišnou od Bézierových křivek nebo animací na plátně.

Použití setInterval

Animaci lze implementovat jako sekvenci snímků – obvykle malé změny vlastností HTML/CSS.

Například změna style.left od 0px do 100px přesune prvek. A pokud jej zvýšíme v setInterval , mění se o 2px s malým zpožděním, například 50krát za sekundu, pak vypadá hladce. To je stejný princip jako v kině:24 snímků za sekundu stačí, aby to vypadalo hladce.

Pseudokód může vypadat takto:

let timer = setInterval(function() {
  if (animation complete) clearInterval(timer);
  else increase style.left by 2px
}, 20); // change by 2px every 20ms, about 50 frames per second

Úplnější příklad animace:

let start = Date.now(); // remember start time

let timer = setInterval(function() {
  // how much time passed from the start?
  let timePassed = Date.now() - start;

  if (timePassed >= 2000) {
    clearInterval(timer); // finish the animation after 2 seconds
    return;
  }

  // draw the animation at the moment timePassed
  draw(timePassed);

}, 20);

// as timePassed goes from 0 to 2000
// left gets values from 0px to 400px
function draw(timePassed) {
  train.style.left = timePassed / 5 + 'px';
}

Klikněte pro ukázku:

Resultindex.html
<!DOCTYPE HTML>
<html>

<head>
  <style>
    #train {
      position: relative;
      cursor: pointer;
    }
  </style>
</head>

<body>

  <img id="train" src="https://js.cx/clipart/train.gif">


  <script>
    train.onclick = function() {
      let start = Date.now();

      let timer = setInterval(function() {
        let timePassed = Date.now() - start;

        train.style.left = timePassed / 5 + 'px';

        if (timePassed > 2000) clearInterval(timer);

      }, 20);
    }
  </script>


</body>

</html>

Použití requestAnimationFrame

Představme si, že běží několik animací současně.

Pokud je spustíme samostatně, pak i když každý má setInterval(..., 20) , pak by prohlížeč musel překreslovat mnohem častěji než každých 20ms .

Je to proto, že mají odlišný čas začátku, takže „každých 20 ms“ se mezi různými animacemi liší. Intervaly nejsou zarovnány. Takže budeme mít několik nezávislých běhů v rámci 20ms .

Jinými slovy, toto:

setInterval(function() {
  animate1();
  animate2();
  animate3();
}, 20)

…Je lehčí než tři nezávislá volání:

setInterval(animate1, 20); // independent animations
setInterval(animate2, 20); // in different places of the script
setInterval(animate3, 20);

Těchto několik nezávislých překreslení by mělo být seskupeno, aby bylo překreslování pro prohlížeč snazší, a tudíž méně zatěžovalo CPU a vypadalo plynuleji.

Je třeba mít na paměti ještě jednu věc. Někdy je CPU přetíženo nebo existují jiné důvody pro méně časté překreslování (například když je karta prohlížeče skrytá), takže bychom to opravdu neměli spouštět každých 20ms .

Ale jak o tom v JavaScriptu víme? Existuje specifikace Časování animace, která poskytuje funkci requestAnimationFrame . Řeší všechny tyto problémy a ještě více.

Syntaxe:

let requestId = requestAnimationFrame(callback)

To naplánuje callback funkci spustit v nejbližším čase, kdy chce prohlížeč provést animaci.

Pokud provedeme změny v prvcích v callback pak budou seskupeny s ostatními requestAnimationFrame zpětná volání a s animacemi CSS. Takže bude jeden přepočet geometrie a překreslení místo mnoha.

Vrácená hodnota requestId lze použít ke zrušení hovoru:

// cancel the scheduled execution of callback
cancelAnimationFrame(requestId);

callback dostane jeden argument – ​​čas, který uplynul od začátku načítání stránky v milisekundách. Tento čas lze také získat voláním performance.now().

Obvykle callback běží velmi brzy, pokud není přetížený procesor nebo baterie notebooku téměř vybitá nebo existuje jiný důvod.

Níže uvedený kód ukazuje čas mezi prvními 10 spuštěními pro requestAnimationFrame . Obvykle je to 10–20 ms:

<script>
  let prev = performance.now();
  let times = 0;

  requestAnimationFrame(function measure(time) {
    document.body.insertAdjacentHTML("beforeEnd", Math.floor(time - prev) + " ");
    prev = time;

    if (times++ < 10) requestAnimationFrame(measure);
  })
</script>

Strukturovaná animace

Nyní můžeme vytvořit univerzálnější animační funkci založenou na requestAnimationFrame :

function animate({timing, draw, duration}) {

  let start = performance.now();

  requestAnimationFrame(function animate(time) {
    // timeFraction goes from 0 to 1
    let timeFraction = (time - start) / duration;
    if (timeFraction > 1) timeFraction = 1;

    // calculate the current animation state
    let progress = timing(timeFraction)

    draw(progress); // draw it

    if (timeFraction < 1) {
      requestAnimationFrame(animate);
    }

  });
}

Funkce animate přijímá 3 parametry, které v podstatě popisují animaci:

duration

Celková doba animace. Třeba 1000 .

timing(timeFraction)

Funkce časování, jako je vlastnost CSS transition-timing-function to dostane zlomek času, který uplynul (0 na začátku 1 na konci) a vrátí dokončení animace (jako y na Bezierově křivce).

Například lineární funkce znamená, že animace probíhá rovnoměrně se stejnou rychlostí:

function linear(timeFraction) {
  return timeFraction;
}

Jeho graf:

To je jako transition-timing-function: linear . Níže jsou uvedeny zajímavější varianty.

draw(progress)

Funkce, která převezme stav dokončení animace a vykreslí jej. Hodnota progress=0 označuje počáteční stav animace a progress=1 – koncový stav.

Toto je funkce, která ve skutečnosti vykresluje animaci.

Může přesunout prvek:

function draw(progress) {
  train.style.left = progress + 'px';
}

…Nebo udělejte cokoli jiného, ​​můžeme animovat cokoli, jakýmkoli způsobem.

Pojďme animovat prvek width z 0 na 100% pomocí naší funkce.

Klikněte na prvek pro ukázku:

Resultanimate.jsindex.html
function animate({duration, draw, timing}) {

  let start = performance.now();

  requestAnimationFrame(function animate(time) {
    let timeFraction = (time - start) / duration;
    if (timeFraction > 1) timeFraction = 1;

    let progress = timing(timeFraction)

    draw(progress);

    if (timeFraction < 1) {
      requestAnimationFrame(animate);
    }

  });
}
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <style>
    progress {
      width: 5%;
    }
  </style>
  <script src="animate.js"></script>
</head>

<body>


  <progress id="elem"></progress>

  <script>
    elem.onclick = function() {
      animate({
        duration: 1000,
        timing: function(timeFraction) {
          return timeFraction;
        },
        draw: function(progress) {
          elem.style.width = progress * 100 + '%';
        }
      });
    };
  </script>


</body>

</html>

Kód:

animate({
  duration: 1000,
  timing(timeFraction) {
    return timeFraction;
  },
  draw(progress) {
    elem.style.width = progress * 100 + '%';
  }
});

Na rozdíl od CSS animace zde můžeme udělat libovolnou funkci časování a jakoukoli kreslicí funkci. Funkce časování není omezena Bézierovými křivkami. A draw může jít za hranice vlastností, vytvářet nové prvky, jako je animace ohňostroje nebo tak něco.

Funkce časování

Nejjednodušší funkci lineárního časování jsme viděli výše.

Podívejme se na více z nich. Vyzkoušíme si animace pohybu s různými funkcemi časování, abychom viděli, jak fungují.

Síla n

Pokud chceme animaci zrychlit, můžeme použít progress v mocnině n .

Například parabolická křivka:

function quad(timeFraction) {
  return Math.pow(timeFraction, 2)
}

Graf:

Podívejte se v akci (klikněte pro aktivaci):

…Nebo kubická křivka nebo ještě větší n . Zvýšením výkonu se zrychlí.

Zde je graf pro progress v mocnině 5 :

V akci:

Oblouk

Funkce:

function circ(timeFraction) {
  return 1 - Math.sin(Math.acos(timeFraction));
}

Graf:

Záda:střelba z luku

Tato funkce provádí „střelbu z luku“. Nejprve „natáhneme tětivu“ a poté „vystřelíme“.

Na rozdíl od předchozích funkcí závisí na dodatečném parametru x , „koeficient pružnosti“. Je jím definována vzdálenost „natažení tětivy“.

Kód:

function back(x, timeFraction) {
  return Math.pow(timeFraction, 2) * ((x + 1) * timeFraction - x)
}

Graf pro x = 1.5 :

Pro animaci jej používáme s konkrétní hodnotou x . Příklad pro x = 1.5 :

Odskok

Představte si, že pouštíme míč. Spadne dolů, pak se několikrát odrazí a zastaví se.

bounce funkce dělá totéž, ale v opačném pořadí:„odrážení“ se spustí okamžitě. K tomu používá několik speciálních koeficientů:

function bounce(timeFraction) {
  for (let a = 0, b = 1; 1; a += b, b /= 2) {
    if (timeFraction >= (7 - 4 * a) / 11) {
      return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2)
    }
  }
}

V akci:

Elastická animace

Další „elastická“ funkce, která přijímá další parametr x pro „počáteční rozsah“.

function elastic(x, timeFraction) {
  return Math.pow(2, 10 * (timeFraction - 1)) * Math.cos(20 * Math.PI * x / 3 * timeFraction)
}

Graf pro x=1.5 :

V akci pro x=1.5 :

Zvrat:snadnost*

Máme tedy sbírku časovacích funkcí. Jejich přímá aplikace se nazývá „easeIn“.

Někdy potřebujeme ukázat animaci v opačném pořadí. To se provádí pomocí transformace „easeOut“.

easeOut

V režimu „easeOut“ timing funkce je vložena do obalu timingEaseOut :

timingEaseOut(timeFraction) = 1 - timing(1 - timeFraction)

Jinými slovy, máme „transformační“ funkci makeEaseOut která přebírá „běžnou“ funkci časování a vrací kolem ní obal:

// accepts a timing function, returns the transformed variant
function makeEaseOut(timing) {
  return function(timeFraction) {
    return 1 - timing(1 - timeFraction);
  }
}

Například můžeme vzít bounce výše popsanou funkci a použijte ji:

let bounceEaseOut = makeEaseOut(bounce);

Odskok pak nebude na začátku, ale na konci animace. Vypadá ještě lépe:

Resultsstyle.cssindex.html
#brick {
  width: 40px;
  height: 20px;
  background: #EE6B47;
  position: relative;
  cursor: pointer;
}

#path {
  outline: 1px solid #E8C48E;
  width: 540px;
  height: 20px;
}
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
  <script src="https://js.cx/libs/animate.js"></script>
</head>

<body>


  <div id="path">
    <div id="brick"></div>
  </div>

  <script>
    function makeEaseOut(timing) {
      return function(timeFraction) {
        return 1 - timing(1 - timeFraction);
      }
    }

    function bounce(timeFraction) {
      for (let a = 0, b = 1; 1; a += b, b /= 2) {
        if (timeFraction >= (7 - 4 * a) / 11) {
          return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2)
        }
      }
    }

    let bounceEaseOut = makeEaseOut(bounce);

    brick.onclick = function() {
      animate({
        duration: 3000,
        timing: bounceEaseOut,
        draw: function(progress) {
          brick.style.left = progress * 500 + 'px';
        }
      });
    };
  </script>


</body>

</html>

Zde můžeme vidět, jak transformace změní chování funkce:

Pokud je na začátku efekt animace, jako je poskakování – zobrazí se na konci.

V grafu výše má pravidelný odraz červenou barvu a mírný odraz je modrý.

  • Pravidelný odraz – objekt se odrazí zespodu a na konci prudce vyskočí nahoru.
  • Po easeOut – nejprve vyskočí na vrchol, pak se tam odrazí.

easeInOut

Můžeme také ukázat efekt jak na začátku, tak na konci animace. Transformace se nazývá „easeInOut“.

Vzhledem k funkci časování vypočítáme stav animace takto:

if (timeFraction <= 0.5) { // first half of the animation
  return timing(2 * timeFraction) / 2;
} else { // second half of the animation
  return (2 - timing(2 * (1 - timeFraction))) / 2;
}

Kód obálky:

function makeEaseInOut(timing) {
  return function(timeFraction) {
    if (timeFraction < .5)
      return timing(2 * timeFraction) / 2;
    else
      return (2 - timing(2 * (1 - timeFraction))) / 2;
  }
}

bounceEaseInOut = makeEaseInOut(bounce);

V akci, bounceEaseInOut :

Resultsstyle.cssindex.html
#brick {
  width: 40px;
  height: 20px;
  background: #EE6B47;
  position: relative;
  cursor: pointer;
}

#path {
  outline: 1px solid #E8C48E;
  width: 540px;
  height: 20px;
}
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
  <script src="https://js.cx/libs/animate.js"></script>
</head>

<body>


  <div id="path">
    <div id="brick"></div>
  </div>

  <script>
    function makeEaseInOut(timing) {
      return function(timeFraction) {
        if (timeFraction < .5)
          return timing(2 * timeFraction) / 2;
        else
          return (2 - timing(2 * (1 - timeFraction))) / 2;
      }
    }


    function bounce(timeFraction) {
      for (let a = 0, b = 1; 1; a += b, b /= 2) {
        if (timeFraction >= (7 - 4 * a) / 11) {
          return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2)
        }
      }
    }

    let bounceEaseInOut = makeEaseInOut(bounce);

    brick.onclick = function() {
      animate({
        duration: 3000,
        timing: bounceEaseInOut,
        draw: function(progress) {
          brick.style.left = progress * 500 + 'px';
        }
      });
    };
  </script>


</body>

</html>

Transformace „easeInOut“ spojuje dva grafy do jednoho:easeIn (běžné) pro první polovinu animace a easeOut (obrácené) – pro druhou část.

Efekt je jasně vidět, pokud porovnáme grafy easeIn , easeOut a easeInOut z circ funkce časování:

  • Červená je běžná varianta circ (easeIn ).
  • Zelená – easeOut .
  • Modrá – easeInOut .

Jak vidíme, graf první poloviny animace je zmenšený easeIn a druhá polovina je zmenšená easeOut . V důsledku toho se animace spustí a skončí se stejným efektem.

Zajímavější „kreslení“

Místo přesunu prvku můžeme udělat něco jiného. Vše, co potřebujeme, je napsat správný draw .

Zde je animovaný „skákající“ text:

Resultsstyle.cssindex.html
textarea {
  display: block;
  border: 1px solid #BBB;
  color: #444;
  font-size: 110%;
}

button {
  margin-top: 10px;
}
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
  <script src="https://js.cx/libs/animate.js"></script>
</head>

<body>


  <textarea id="textExample" rows="5" cols="60">He took his vorpal sword in hand:
Long time the manxome foe he sought—
So rested he by the Tumtum tree,
And stood awhile in thought.
  </textarea>

  <button onclick="animateText(textExample)">Run the animated typing!</button>

  <script>
    function animateText(textArea) {
      let text = textArea.value;
      let to = text.length,
        from = 0;

      animate({
        duration: 5000,
        timing: bounce,
        draw: function(progress) {
          let result = (to - from) * progress + from;
          textArea.value = text.slice(0, Math.ceil(result))
        }
      });
    }


    function bounce(timeFraction) {
      for (let a = 0, b = 1; 1; a += b, b /= 2) {
        if (timeFraction >= (7 - 4 * a) / 11) {
          return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2)
        }
      }
    }
  </script>


</body>

</html>

Shrnutí

U animací, se kterými si CSS neumí dobře poradit, nebo u těch, které vyžadují přísnou kontrolu, může pomoci JavaScript. Animace JavaScriptu by měly být implementovány pomocí requestAnimationFrame . Tato vestavěná metoda umožňuje nastavit funkci zpětného volání, která se spustí, když bude prohlížeč připravovat překreslení. Obvykle je to velmi brzy, ale přesný čas závisí na prohlížeči.

Když je stránka na pozadí, nedochází k žádnému překreslování, takže zpětné volání se nespustí:animace bude pozastavena a nebude spotřebovávat zdroje. To je skvělé.

Zde je pomocník animate funkce pro nastavení většiny animací:

function animate({timing, draw, duration}) {

  let start = performance.now();

  requestAnimationFrame(function animate(time) {
    // timeFraction goes from 0 to 1
    let timeFraction = (time - start) / duration;
    if (timeFraction > 1) timeFraction = 1;

    // calculate the current animation state
    let progress = timing(timeFraction);

    draw(progress); // draw it

    if (timeFraction < 1) {
      requestAnimationFrame(animate);
    }

  });
}

Možnosti:

  • duration – celkový čas animace v ms.
  • timing – funkce pro výpočet průběhu animace. Získá časový zlomek od 0 do 1, vrátí průběh animace, obvykle od 0 do 1.
  • draw – funkce pro kreslení animace.

Určitě bychom to mohli vylepšit, přidat další zvonky a píšťalky, ale JavaScriptové animace se nepoužívají na denní bázi. Jsou zvyklí dělat něco zajímavého a nestandardního. Takže byste chtěli přidat funkce, které potřebujete, když je potřebujete.

Animace JavaScriptu mohou používat jakoukoli funkci časování. Probrali jsme spoustu příkladů a transformací, aby byly ještě univerzálnější. Na rozdíl od CSS zde nejsme omezeni na Bezierovy křivky.

Totéž se týká draw :můžeme animovat cokoli, nejen vlastnosti CSS.