SVG: plynulá animace s minimálními nároky

Rozhodl jsem se vyšperkovat homepage animovaným neonem. Videu jsem se však chtěl vyhnout kvůli datové náročnosti, nutnosti řešit rozlišení i složité responzivitě. Vhodným řešením byla SVG animace, která se ovšem nepodařila „na první dobrou“. Sekala se.


Úskalím bylo, že jsem pro výsledný vizuál potřeboval aplikovat více efektů:

  • rozostření podporující dojem neonové zářivky,
  • vržený stín simulující dopadající světlo,
  • animaci rozbité poblikávající tečky.

Všech tří vlastností lze dosáhnout několika způsoby a jejich různé kombinace buď není možné animovat plynule nebo velmi vytěžují počítač. První verze založená na postupu 9elements přesně tímto problémem trpěla.

V praxi to pak vypadalo, že mi díky roztočenému větráku uletí počítač zpod rukou. Vizuální stránku problému demonstruje video porovnávající dva hlavní způsoby animace SVG.

Porovnání animace obalového <divu> (dole) a přímé animace <svg> (nahoře)

K jádru problému mě dovedl Charlie Marsh, který podobnou věc řešil pro Khan Academy:

In other words, applying linear transformations to SVG elements does trigger re-layout and re-painting.


Problém: přímá animace SVG

Z testů pak vyplynulo, že problém nastane, když do inline SVG vložím přímo CSS animaci:

(...)
</div>
<svg viewBox="0 0 767 194" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <style>
      @keyframes blink {
        10% {opacity: 1;}
        11% {opacity: 0.2;}
        (...)
        92.5% {opacity: 1;}
    }

    #dot {
      animation: blink linear infinite 7s;
    }
  </style>
(...)

Stejně jako v případě SMIL animace přes <animate>:

(...)
</div>
<svg viewBox="0 0 767 194" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <polygon points="440 142 415 142 415 117 440 117">
    <animate attributeType="CSS" attributeName="opacity" from="1" to="0" dur="5s" repeatCount="indefinite" />
  </polygon>
(...)

Zkrátka a dobře jsou s přímou animací problémy, ačkoliv by oba způsoby byly vhodné. Vytvořila by se tím jednoduchá komponenta, jež bych mohl bez větší práce házet, kam si zamanu.

Bohužel však byl vliv na výkon nezanedbatelný. Zároveň, jak je vidět na videu, docházelo v neonu k barevné odchylce, přestože oba používají stejnou proměnnou. Nevím, z čeho přesně chyba pramenila, ale vzhledem k tomu, že stejně nešlo o vhodnou cestu, jsem se do řešení ani nepouštěl. Klidně mohlo jít o něco se správou barev a tomu se milerád vyhnu.

Řešení: CSS animace obaleného SVG

Očistil jsem tedy SVG od zbytečného balastu, rozdělil na dvě a každé obalil do <divu>. Animaci pak jen stačilo hodit na div.neon-dot:

<div class="neon-dot">
  <svg viewBox="0 0 767 194" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <polygon points="440 142 415 142 415 117 440 117"></polygon>
  </svg>
</div>

Spolu s tím jsem rovnou smazal prasácké SVG efekty feGaussianBlurfeColorMatrix, které po exportu ze Sketche (a optimalizaci!) vypadaly nějak takto…:

<filter x="0" y="0" width="100%" height="100%" filterUnits="userSpaceOnUse" id="blur">
  <feGaussianBlur stdDeviation="0.6" in="SourceGraphic"></feGaussianBlur>
</filter>
<filter x="0" y="0" width="100%" height="100%" filterUnits="userSpaceOnUse" id="neon">
    <feMorphology radius="2" operator="dilate" in="SourceAlpha" result="shadowSpreadOuter1"></feMorphology>
    <feOffset dx="0" dy="3" in="shadowSpreadOuter1" result="shadowOffsetOuter1"></feOffset>
    <feMorphology radius="2" operator="erode" in="SourceAlpha" result="shadowInner"></feMorphology>
    <feOffset dx="0" dy="3" in="shadowInner" result="shadowInner"></feOffset>
    <feComposite in="shadowOffsetOuter1" in2="shadowInner" operator="out" result="shadowOffsetOuter1"></feComposite>
    <feGaussianBlur stdDeviation="3" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
    (...)
    <feColorMatrix values="0 0 0 0 1   0 0 0 0 0.666666667   0 0 0 0 0  0 0 0 0.4 0" type="matrix" in="shadowBlurOuter4" result="shadowMatrixOuter4"></feColorMatrix>
    <feMerge>
        <feMergeNode in="shadowMatrixOuter1"></feMergeNode>
        <feMergeNode in="shadowMatrixOuter2"></feMergeNode>
        <feMergeNode in="shadowMatrixOuter3"></feMergeNode>
        <feMergeNode in="shadowMatrixOuter4"></feMergeNode>
    </feMerge>
</filter>

…a nahradil přehlednějším a jednořádkovým CSS filtrem:

.neon-text,
.neon-dot {
  filter: blur(3px) drop-shadow(0 4px 40px var(--highlight-yellow-neon)) drop-shadow(0 4px 8px var(--highlight-yellow-neon));
}

Tím se vyřešily veškeré problémy s výkonem, barvou i editovatelností. 🏆