Podzim s SVG: Listopad

Krásné barevné listí na stromech, na zemi a zejména pak jejich krátká (ale také poslední) pouť z větví na zem je jednou z největších krás celého podzimu. Jak by takový listopad mohl vypadat v podání technologie SVG, si ukážeme právě dnes.

Seriál: Podzim s SVG (3 díly)

  1. Podzim s SVG: Dlabání dýně 29.10.2014
  2. Podzim s SVG: Listopad 5.11.2014
  3. Podzim s SVG: Morphing alias Morfing 12.11.2014

Za prvé potřebujeme vektorový obrázek nějakého toho lístečku. Já opět použil Google hledání obrázků s určením druhu formátu ("filetype:SVG leaf") a vzápětí si svého kandidáta vybral a stáhnul. Po (pro mne) nezbytném zjednodušení křivek původního obrázku a zrušení přebytečných skupin v Inkscape máme výchozí obrázek, se kterým budeme dále pracovat.

Lístek

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <g stroke-width="2" stroke="black" fill="green">
        <path d="M266,371c-8-4-17-7-19-16-9-14-10-30-16-44-4-10-8-20-12-30-12..."/>
        <path d="M195,207c-13,35-25,69-40,103-1,6-2,13-3,19"/>
        <!-- několik dalších křivek žilkování listu -->
    </g>
</svg>

SVG nezná nic takového, jako je box model používaný v HTML, a tak budeme-li chtít listem otáčet kolem jeho vlastního středu, přesuneme si jej (atribut transform="translate(x, y)") středem do středu SVG canvasu, tj. souřadnic x=0, y=0 (bohatě stačí od oka, přibližně). V Inkscape si také nakreslíme křivku pohybu padajícího listu, např. oblouk zleva doprava a zase zpět. Do dokumentu přidáme oblast definic defs, list do ní přesuneme, vnoříme jej do další skupiny g (toto je nutné pro vytvoření nového relativního viewPortu, který nám zajistí zachování otáčení kolem středu listu); této skupině přiřadíme identifikátor id="leaf" a nakonec přidáme do extra path elementu i pohybovou křivku vytvořenou v editoru (stačí i jen překopírovat přímo přes clipboard z Inkscape, bez ukládání nového souboru na disk), které rovněž přidáme identifikátor (id="fly") pro potřebné pozdější referencování.

<defs>
    <g id="leaf">
        <g stroke-width="2" transform="translate(-350,-170)">
            <path d="M266,371c-8-4-17-7-19-16-9-14-10-30-16-44-4-10-8-20-12-30..."/>
            <path d="M195,207c-13,35-25,69-40,103-1,6-2,13-3,19"/>
            <!-- několik dalších křivek -->
        </g>
    </g>
 
    <path id="fly" d="M 5.71,9.51 C 5.71,9.51 189,199 269,9.51 269,9.51 203,154 5.71,9.51 z"/>
</defs>

Pozn: protože budeme chtít později náhodně generovat barvu výplně fill i čáry stroke, tak je z definice listu odebereme již nyní.

Pohyb listu

Pro animaci použijeme i v tomto příkladě animace SMIL. Konkrétně budou zapotřebí dvě pro každý jeden list. Jedna oblouková animace zleva doprava podle nakreslené křivky fly, které dosáhneme použitím speciálního animačního elementu animateMotion, s referencí požadované křivky vnořeným elementem mpath. Druhý pohyb bude prostý animovaný přesun animateTransform s typem translate shora dolů, který pro ještě lepší efekt neuděláme pouze vertikální, ale spíše mírně sešikmený (atribut to pro souřadnice x,y).

Zapouzdření listu (použitého pomocí známého elementu use) pro finální použití v šabloně id="one" provádíme z toho důvodu, že na jeden element může být najednou aplikována zase pouze jediná animovaná transformace. Takže chceme-li jich mít více najednou (dvě v našem případě, tj. pohyb zleva doprava a pohyb zhora dolů), je nejsnažším a nejbezpečnějším způsobem právě „matrjoškování“, tj.vnořování skupin do sebe.

<defs>
    <g id="leaf"><!-- definice listu --></g>
 
    <path id="fly" d="M 5.71,9.51 C 5.71,9.51 189,199 269,9.51 269,9.51 203,154 5.71,9.51 z"/>

    <animateTransform id="fall" attributeName="transform" type="translate" begin="0s" dur="6s" repeatCount="1" fill="freeze" to="0 600"/>

    <g id="one">
        <use xlink:href="#leaf" transform="scale(0.3) rotate(90)" fill="green">
            <animateMotion dur="3" repeatCount="2" fill="freeze">
                <mpath xlink:href="#fly"/>
            </animateMotion>
        </use>
    </g>
</defs>

Pozn: obě animace máme definovány pouze jako šablony a jejich skutečné hodnoty budeme přepisovat dynamicky během generování scriptem.

Než přistoupíme k finálnímu scriptování, vytvoříme si v dokumentu ještě poslední dva elementy. Obdélník rect pro pozadí celé scény s barevným přechodem linearGradient id="back", navozující temnými barvami podzimní atmosféru, na který který navíc bude zavěšena událost onclick, načítající nanovo celý dokument a spouštějící tak znovu celou animaci.

Další, co ještě potřebujeme, je prázdná skupina g id="canvas", do které budeme vygenerované listy umísťovat. Určíme si zároveň také cílovou velikost pracovního plátna na 700x400px (velikost rect elementu, kterou atributem viewBox="0 0 700 400" určíme v kořenovém elementu dokumentu), ze které budeme při našem náhodném generování hodnot vycházet.

<defs>
    <linearGradient id="back" gradientUnits="objectBoundingBox" x1="0" y1="0" x2="0" y2="1">
        <stop offset="0" stop-color="brown"/>
        <stop offset="1" stop-color="black"/>
    </linearGradient>
</defs>

<rect x="0" y="0" width="700" height="400" fill="url(#back)" rx="20" onclick="window.location.reload()"/>

<g id="canvas"/>

Přichází chvíle pro závěrečné scriptování. Nebude ale složité, neboť se budeme snažit ovlivnit poměrně hodně vlastností padajícího listu najednou (za masivního používání generátoru náhodných čísel), a může se někomu snad zdát pro jeho délku poněkud náročné na čtení. Ale není důvod se vůbec bát.

Nejprve si připravíme do proměnných potřebné šablony (leaf pro list, leafFall pro animaci pohybu listu shora dolů a canvas jako kontejner pro všechny listy), odebereme jim pro následnou multiplikaci atributy jedinečných identifikátorů a využijeme cyklus for pro generování jednotlivých listů, v našem případě jich bude padesát (v dalším popisu kódu se už budeme věnovat pouze těle cyklu samotného).

<script>
    var leaf = document.getElementById("one"), 
        leafFall = document.getElementById("fall"), 
        canvas = document.getElementById("canvas");
    leaf.removeAttribute("id"); 
    leafFall.removeAttribute("id");
    
    for (var i=0; i&lt;50; i++) {
        // tělo cyklu viz dále
    }
</script>

Pozn: Pokud není v XML (SVG) dokumentu celý script uvozen jako entita CDATA, znaménko menší-než v podmínce cyklu musí být zapsáno jako entita &lt;.

Na celém generování je asi nejsložitější vymezení mezí, ve kterých chceme, aby se náhodně generovaná čísla pohybovala. Začátek obou animací pro jeden list begin chceme mezi 0-4 sekundami, délku duration jednoho obloukového pohybu (kmitu) listu zleva doprava mezi 2-5 sekundami a celkový počet kmitů swings během pádu shora dolů mezi 1-3 kmity. V proměnných one, use, move a fall máme novou instanci listu, referenci na vnitřní list samotný, referenci na obloukovou animaci listu animateMotion zleva doprava a referenci na vertikální animaci pádu listu dolů animateTransform.

    var begin = Math.random()*4;
    var duration = Math.round(2+Math.random()*3);
    var swings = Math.round(1+Math.random()*2);

    var one = leaf.cloneNode(true);
    var use = one.firstChild;
    var move = use.firstChild;
    var fall = leafFall.cloneNode(true);

Pozn: abychom mohli použít zjednodušenou selekci elementů skrze property .firstChild, obětovali jsme v části zápisu pro g id="one" odsazování některých elementů.

V dalším kódu začínáme přepisovat hodnoty atributů nové instance listu, řádek po řádku:

  • náhodná výchozí pozice listu transform(translate) v souřadnicích x y
  • náhodná výplň fill barvy listu v plné škále barevného modelu RGB
  • náhodná barva linky stroke listu ve škále tmavších barev (opět modelem RGB)
  • náhodná velikost a natočení listu transform(scale rotate)
  • přiřazení délky dur nové animace kmitu listu zleva doprava
  • počtu jejího opakování releatCount
  • a času begin, kdy má vůbec animace začít.
    one.setAttribute("transform", "translate("+(20+Math.random()*570)+" "+((Math.random()*-10)-1)+")");

    use.setAttribute("fill", "rgb("+Math.round((Math.random()*255))+","+Math.round((Math.random()*255))+","+Math.round((Math.random()*255))+")");
    use.setAttribute("stroke", "rgb("+Math.round((Math.random()*66))+","+Math.round((Math.random()*66))+","+Math.round((Math.random()*66))+")");
    use.setAttribute("transform", "scale("+(((Math.random()*2)/10)+0.1)+") rotate("+(Math.random()*360)+")");

    move.setAttribute("dur", duration);
    move.setAttribute("repeatCount", swings);
    move.setAttribute("begin", begin);

Už jsme skoro na konci a chybí nám toho opravdu už jen málo, takže ještě jednou tedy, řádek po řádku:

  • přiřazení délky dur animace pádu listu dolů, což je násobek počtu kmitů a délky trvání kmitu
  • začátek animace pádu begin, který chceme mít stejný, jako u kmitavé (houpavé) horizontální animace (Pozn: Chrome obsahuje v současnosti regresní chybu, týkající se začátku časování vnořených animací)
  • vygenerovat náhodně místo dopadu listu (atribut to na souřadnicích x y)
  • přidat modifikovanou novou animaci fall k novému listu
  • no a konečně nový list samotný přidat do dokumentu, jmenovitě do renderovaného elementu g id="canvas".
    fall.setAttribute("dur", duration*swings);
    fall.setAttribute("begin", begin);
    fall.setAttribute("to", ""+(20+Math.random()*570)+" "+((Math.random()*100)+260));

    one.appendChild(fall);
    canvas.appendChild(one);

Finální dílo, fullscreen v novém okně

Tak a to je všechno. Animaci máme hotovou, na každý klik se vygeneruje mírně odlišně a budeme-li chtít, můžeme pomocí ní sezónně vyzdobit titulní stránku našeho oblíbeného webového projektu. Animace je dobře funkční ve všech moderních webových prohlížečích s výjimkou Internet Exploreru, pro který by přidání SMIL kompatibilního kódu neúměrně zvyšilo komplexitu (a dosti snížilo čitelnost) celého ukázkového kódu.

Těším se brzy zase možná na shledanou v dalším díle Podzimu s SVG.

Kompletní ukázka finálního kódu

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 700 400">
    <title>Autumn Leaf Fall, by Marek Raida</title>

    <defs>
    <g id="leaf">
        <g stroke-width="2" transform="translate(-350,-170)">
            <path d="M266,371c-8-4-17-7-19-16-9-14-10-30-16-44-4-10-8-20-12-30-12,13-24,25-39,35-13,8-23,19-26,34-2,10-5,13-8,1-3-18-1-37,1-55,7-27,9-56,10-85,6-11,9-26,21-33,7-3,12-13,20-12,9,1,15,6,12,16-3,9-7,18-13,25,10-9,18-21,20-35,15,3,31,6,41,19,9,9,12,23,13,36,3,3,9,20,7,17-4-7-3-15-5-23-1-14-5-29-15-40,21,5,42,10,62,16,9-2-10-6-14-7-11-4-23-8-35-10,21-10,45-13,64-27,10-6,22-11,34-11-17,3-34,6-50,14-22,10-45,19-69,20-9-3-18-5-27-7,17-11,29-28,36-46,5-9,9-16,0-4-10,10-14,25-26,34-6,6-12,11-21,11-6,0-18-2-14-12,3-15,9-30,15-45,8-10.9,16-22.3,28-28.7,19-13.6,39-26.9,58-40.2,1,13.4-2,27.4,1,40.3,5,12,13,24.6,7,37.6,13-3,25-7,36-15,15-9.5,30-20.1,49-21.2,8-1.8,16,0.5,22,6,1,8.4-13-0.2-15,11.3-10,8.9,9,2.9,15,3.9,15-1.2,33-2.5,45,9,16,10,32,22,44,38-1,7,3,22-6,11-14-2-26,8-38,12-5,4-20,6-17,12,19,15,41,27,56,46,17,14,35,28,49,45,11,12,14,28,22,42,6,19,9,39,13,59,6,14-5,3-11-1-20-15-42-25-65-36-15-7-32-2-48-2-25,1-51,2-76,0,5,18,6,37,17,53,10,15,14,32,15,50,1,12-9-3-15-5-16-13-38-13-56-24-19-8-38-19-49-36-6-7-10-14-15-21-3,6-4,19-3,22-3-2-7-3-10-4z M260,118c-11,11-15,26-27,35-6,6-12,11-21,11-13,1-22-13-35-14-27-8-53-20-73-39-12.9-14.5-21.6-32-32.8-47.4-1.8,7.5-11.5,15.3-7.2,22.5,7.1,12.1,14.1,23.9,25.6,32.9,17.4,10,33.4,24,52.4,30,20,7,41,10,61,19,10-1,8,12,5,19s-6,14-11,20c10-9,18-21,20-35,15,3,31,6,41,19,9,9,12,23,13,36,3,3,9,20,7,17-4-7-3-15-5-23-1-14-5-29-15-40,21,5,42,10,62,16,9-2-10-6-14-7-11-4-23-8-35-10,21-10,45-13,64-27,10-6,22-11,34-11-17,3-34,6-50,14-22,10-45,19-69,20-9-3-18-5-27-7,18-12,31-30,37-51z"/>
            <path d="M195,207c-13,35-25,69-40,103-1,6-2,13-3,19"/>
            <path d="M279,246c8,25,16,51,25,76,14,18,26,38,41,55,20,17,39,35,59,52"/>
            <path d="M283,262c-7,12-17,23-23,35,0,19-2,39,4,57,1,1,2,12,2,7"/>
            <path d="M260,337c-2,7-5,14-7,21"/>
            <<path d="M292,286c17,10,35,20,52,30"/>
            <path d="M324,196c35,20,70,41,107,57,24,11,48,22,70,37,11,6,21,14,28,24,13,14,25,28,38,42"/>
            <path d="M357,218c13,18,23,39,40,53,11,10,21,20,32,30h-1-1"/>
            <path d="M384,259c1,19,1,37,2,56h1"/>
            <path d="M406,242c21,0,42-2,63-2,13,6,27,11,40,16"/>
            <path d="M369,140c23-4,45-10,69-8,15-1,30,4,45,6,6,5,11,11,17,16"/>
            <path d="M371,140c24,6,49,11,73,17"/>
            <path d="M399,148c10,15,20,29,30,44"/>
            <path d="M348,145c7-14,11-30,23-41,7-10.5,18-14.7,28-19.8,2,0.5,3,1.1,5,1.6"/>
            <path d="M357,124c12-3,24-7,36-10"/>
            <path d="M350,138c-4-8-11-16-8-25v-5"/>
            <path d="M266,111c10-21.1,20-42.2,30-63.3"/>
            <path d="M247,135c-2-15-5-30-7-44.4"/>
            <path d="M258,122c14-6,29-13,43-19"/>
        </g>
    </g>

    <linearGradient id="back" gradientUnits="objectBoundingBox" x1="0" y1="0" x2="0" y2="1">
        <stop offset="0" stop-color="brown"/>
        <stop offset="1" stop-color="black"/>
    </linearGradient>

    <path id="fly" d="M 5.71,9.51 C 5.71,9.51 189,199 269,9.51 269,9.51 203,154 5.71,9.51 z"/>

    <animateTransform id="fall" attributeName="transform" type="translate" begin="0s" dur="6s" repeatCount="1" fill="freeze" to="0 600"/>

    <g id="one"><use xlink:href="#leaf" transform="scale(0.3) rotate(90)"><animateMotion 
            dur="3" repeatCount="2" fill="freeze">
                <mpath xlink:href="#fly"/>
            </animateMotion>
        </use>
    </g>
    </defs>

    <rect x="0" y="0" width="700" height="400" fill="url(#back)" rx="20" onclick="window.location.reload()"/>

    <g id="canvas"/>

    <script>
        var leaf = document.getElementById("one"), 
            leafFall = document.getElementById("fall"), 
            canvas = document.getElementById("canvas");
        leaf.removeAttribute("id");
        leafFall.removeAttribute("id");

        for (var i=0; i&lt;50; i++) {
            var begin = Math.random()*4;
            var duration = Math.round(2+Math.random()*3);
            var swings = Math.round(1+Math.random()*2);

            var one = leaf.cloneNode(true);
            var use = one.firstChild;
            var move = use.firstChild;
            var fall = leafFall.cloneNode(true);
            
            one.setAttribute("transform", "translate("+(20+Math.random()*570)+" "+((Math.random()*-10)-1)+")");
            use.setAttribute("fill", "rgb("+Math.round((Math.random()*255))+","+Math.round((Math.random()*255))+","+Math.round((Math.random()*255))+")");
            use.setAttribute("stroke", "rgb("+Math.round((Math.random()*66))+","+Math.round((Math.random()*66))+","+Math.round((Math.random()*66))+")");
            use.setAttribute("transform", "scale("+(((Math.random()*2)/10)+0.1)+") rotate("+(Math.random()*360)+")");
            move.setAttribute("dur", duration);
            move.setAttribute("repeatCount", swings);
            move.setAttribute("begin", begin);

            fall.setAttribute("dur", duration*swings);
            fall.setAttribute("begin", begin);
            fall.setAttribute("to", ""+(20+Math.random()*570)+" "+((Math.random()*100)+260));
            one.appendChild(fall);
            canvas.appendChild(one);
        }
    </script>
</svg>

SW architekt ve společnosti LMC (jobs.cz, prace.cz), dříve web teamleader ve společnostech IBM, Vodafone apod.  Soukromě se věnuje tvorbě jednoduchých her a dem postavených na webových technologiích, zejména pak SVG grafice.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 9

Přehled komentářů

jan.vilimek autorská práva
Marek Raida Re: autorská práva
vaclav.sir Trochu trhaná animace
Marek Raida Re: Trochu trhaná animace
vaclav.sir Re: Trochu trhaná animace
vaclav.sir Re: Trochu trhaná animace
Error414- Re: Trochu trhaná animace
Marek Raida Re: Trochu trhaná animace
Robert ano, toto internet potřeboval
Zdroj: https://www.zdrojak.cz/?p=13780