Podzim s SVG: Dlabání dýně

K podzimu nepochybně patří krásné barevné dýně. Ty dlabané halloweenské se k nám dostaly z anglosaských zemí, s výrazným nástupem po Sametové revoluci. Pojďme si nyní krok za krokem ukázat, jak si takovou pěknou dýni vydlabat (vyřezat) s technologií SVG.

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

Jako první potřebujeme nějaký vektorový obrázek pěkné dýně. Já osobně použil Google hledání obrázků s určením požadovaného druhu ("filetype:SVG pumpkin") a po chvilce si z nabídky volně dostupných clipartů vybral. Obecně nemám rád „smetí“, které po sobě Inkscape a další editory v dokumentu nechávají, a tak jsem si obrázek nejprve vyčistil a zjednodušil pomocí nástroje Petera Collingridge, pak ručně trochu „učesal“ a tady je výsledek, jak grafický, tak zdrojový kód.

Dýně

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <g id="pumpkin">
        <path d="M167,74c6-21,12-43,24-61-6-3-12-5-18-8-3,23-8,46-6,69z" fill="#f06205"/>
        <path d="M170,41c41-9,88-11,124,14,32,20,44,62,35,97-13,52-60,91-111,102-55,13-119z" fill="#f86707"/>
        <path d="M165,40c35,15,71,33,91,66,15,23,11,53-3,75-18,30-46,53-70,78,47-20,89-59z" fill="#ff7316"/>
        <!-- několik dalších křivek -->
    </g>
</svg>

Dále postupujeme takto: Inspektorem z Vývojářských nástrojů v prohlížeči najdeme křivku path s obrysem celé dýně, kterou budeme potřebovat pro efekt vnitřku vydlabané dýně s hořící svíčkou. Tuto křivku budeme potřebovat kromě vnějšího tvaru ještě jednou. Proto ji z původního umístění přesuneme mimo dýni, přidáme ji identifikátor id="outline" a na původní místo „zkopírujeme“ elementem use, kterému dáme i barevnou výplň fill původní křivky, kterou budeme podruhé potřebovat s výplní úplně jinou (kořenový element svg již nebude v dalších ukázkách kódu pro lepší přehlednost uváděn).

    <g id="pumpkin">
        <path d="M167,74c6-21,12-43,24-61-6-3-12-5-18-8-3,23-8,46-6,69z" fill="#f06205"/>
        <use xlink:href="#outline" fill="#f86707"/>
        <path d="M165,40c35,15,71,33,91,66,15,23,11,53-3,75-18,30-46,53-70,78,47-20,89-59z" fill="#ff7316"/>
        <!-- několik dalších křivek -->
    </g>
    
    <path id="outline" d="M170,41c41-9,88-11,124,14,32,20,44,62,35,97-13,52-60,91-111,102-55,13-119z" fill="#f86707"/>

Pozn: v deklarativním jazyce, jako je SVG, nijak nevadí, když máme použití outline referencováno dříve než samotnou definici.

Nyní potřebujeme vytvořit efekt svíčky uvnitř vydlabané dýně. Na to nám postačí definovat kruhový přechod radialGradient, který bude mít přechod ze žluté ve středu k černé barvě na okrajích, bude mít svůj identifikátor id="back", bude se velikostně (procentuálně) přizpůsobovat velikosti objektu, na který je aplikován (gradientUnits="objectBoundingBox"), a bude centrovaný v obou osách (cx = cy = 0.5). Pomocí identifikátoru jej budeme aplikovat jako výplň fill křivky outline mimo samotnou dýni. Ale protože nemůžeme manipulovat přímo s výplní křivky samotné (neboť by při použití „zkopírování“ již nešla přetížit, což potřebujeme), využijeme dědičnosti, křivku přiřadíme do skupiny g a výplň s přechodem budeme aplikovat na celou skupinu.

    <defs>
        <radialGradient id="back" gradientUnits="objectBoundingBox" cx="0.5" cy="0.5">
            <stop offset="0" stop-color="gold"/>
            <stop offset="0.8" stop-color="black"/>
        </radialGradient>
    </defs>

    <g id="pumpkin">
        <!-- definice dýně, dále se již nemění -->
    </g>
    
    <g fill="url(#back)">
        <path id="outline" d="M170,41c41-9,88-11,124,14,32,20,44,62,35,97-13,52-60,91-111,102-55,13-119z" fill="#f86707"/>
    </g>

Pozn: oblast definic defs znamená, že její prvky nejsou přímo „vykreslovány“, ale pouze definovány pro další použití (Shadow DOM).

Nově vytvořená skupina (s jediným členem) v popředí, by měla zobrazovat jen vyřezané otvory do dýně a nic jiného, na což použijeme efekt maskování mask. Masku definujeme v oblasti definic s identifikátorem id="mask" a bude zpočátku prázdná – vyřezané otvory do ní budeme přidávat scriptováním v podobě křivek za malou chvíli. Maska se chová podobně jako skupina objektů – ty bez výplně nebo s černou výplní se nevykreslují vůbec (maskují), ty s bílou vyplní se naopak vykreslují, a ty s jinou barvou definují poloprůhlednost (v tomto příkladě nebudeme využívat). My budeme do masky během vykreslování (provádění řezu) přidávat křivky nejprve bez výplně (pouze s viditelným okrajem stroke) a při ukončení „řezu“ přidáme bílou vyplň (čímž vykrojená část „vypadne“ a vynikne průhled dovnitř dýně).

Skupina s maskou je kvůli viditelnosti v popředí, a tak pokud chceme zachytávat události řezání na viditelné barevné dýni v pozadí, musíme skupině v popředí označit práce s událostmi atributem pointer-events="none".

    <defs>
        <radialGradient id="back" gradientUnits="objectBoundingBox" cx="0.5" cy="0.5">
            <stop offset="0" stop-color="gold"/>
            <stop offset="0.5" stop-color="black"/>
        </radialGradient>
        
        <mask id="mask" stroke="white" stroke-linejoin="round" stroke-width="2" fill="none"/>
    </defs>

    <!-- definice dýně -->
    
    <g fill="url(#back)" pointer-events="none" mask="url(#mask)">
        <path id="outline" d="M170,41c41-9,88-11,124,14,32,20,44,62,35,97-13,52-60,91-111,102-55,13-119z" fill="#f86707"/>
    </g>

Skoro jediné, co nám ještě zbývá, je přidat do dokumentu script, který bude na elementu dýně (v proměnné pumpkin) zachytávat začátek, pohyb a konec řezu. V dnešní době již nesmíme vynechávat aspoň zjednodušenou podporu také pro dotyková zařízení, což nám kód sice poněkud prodlužuje, ale rozhodně se vyplatí. Aktuální řez (nově vytvářená křivka v proměnné carving) je vlastně řetězec mnoha malých čar (v atributu d, příkaz L) mezi body polygonu na souřadnicích x,y a pro uzavření křivky (při konci řezu) zpět do počátečního bodu přidáme na samotný konec příkaz Z. Všechny řezy přidáváme jako potomky do definované, původně prázdné masky (v proměnné mask).

    <script>
        var pumpkin = document.getElementById("pumpkin"),
            mask = document.getElementById("mask"),
            carving;

        pumpkin.onmousedown = pumpkin.ontouchstart = function(evt) {
            evt.preventDefault();
            carving = document.createElementNS("http://www.w3.org/2000/svg", "path");
            carving.setAttribute("d", "M" + getCoords(evt) + " L" + getCoords(evt));
            mask.appendChild(carving);
        }

        pumpkin.onmousemove = pumpkin.ontouchmove = function(evt) {
            if (!carving) return;
            carving.setAttribute("d", carving.getAttribute("d") + getCoords(evt));
        }

        pumpkin.onmouseup = pumpkin.ontouchend = pumpkin.ontouchcancel = function(evt) {
            if (!carving) return;
            carving.setAttribute("d", carving.getAttribute("d") + " z");
            carving.setAttribute("fill", "white");
            carving = null;
        }
    </script>

Pozn: dobře známé DOM metody jako getElementById(), createElementNS, getAttribute(), setAttribute() či appendChild() zajisté není třeba představovat.

Těm pozornějším z vás, v kódu zajisté neuniklo použití nikde nedefinované funkce getCoords(). Ano, toto je pomocná funkce, zastřešující pro nás vstupní rozdíly mezi myší a dotekem, vracející koordináty již v potřebné podobě formátovaného řetězce s mezerami. Zároveň, pro komfortnější opakování celého procesu řezání nové dýně, přidáme ještě funkci vyprázdňující naplněnou masku s řezy (smazání všech jejích potomků) a do dokumentu i element červeného kruhu circle, který mazací funkci reset() vyvolává.

    <circle cx="20" cy="20" r="16" stroke="black" stroke-width="3" fill="red" onclick="reset()" title="Reset current carving"/>
    
    <script>
        function getCoords(evt) {
            return (evt.touches) ?
                " " + evt.touches[0].pageX + " " + evt.touches[0].pageY :
                " " + evt.clientX + " " + evt.clientY;
        }
        
        function reset() {
            while (mask.hasChildNodes()) mask.removeChild(mask.firstChild);
        }
    <script>
    

Tím jsme se statickou kresbou hotovi. Ale jako třešničku na dort přidáme ještě efekt mihotajícího se světla svíčky (kruhovému přechodu na pozadí), pro který použijeme animační jazyk SMIL a element animate. Přidáme poskakující horizontální mihotání plamene (atribut cx a nelineární přechod mezi mezi požadovanými hodnotami values vynutíme atributem calcMode="discrete"). Světlo by mělo také pohasínat a zase zesilovat, na což použijeme druhou animaci s lineárním rozšiřováním a zužováním rozsahu (offset) černé barvy přechodu (elementu stop) vnitřní části dýně. První animace bude velmi rychlá, druhá pomalejší (hodnota atributu dur).

    <defs>
        <radialGradient id="back" gradientUnits="objectBoundingBox" cx="0.5" cy="0.5">
            <animate attributeName="cx" values="0.55;0.45;0.45;0.5" dur="0.25s" begin="0" calcMode="discrete" repeatCount="indefinite"/>
            <stop offset="0" stop-color="gold"/>
            <stop offset="0.5" stop-color="black">
                <animate attributeName="offset" values="0.8;1;0.9;0.8" dur="3s" begin="0" repeatCount="indefinite"/>
            </stop>
        </radialGradient>
        
        <mask id="mask" stroke="white" stroke-linejoin="round" stroke-width="2" fill="none"/>
    </defs>

Finální dílo s ukázkovou řezbou

A to už je opravdu všechno. Abychom zajistili animační SMIL efekty i v prohlížeči MSIE (který je jako jediný z majoritních nepodporuje), přilinkujeme ještě emulační script smil.user.js, přidáme titulek a máme definitivně hotovo.

Těším se brzy zase 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">
    <title>Halloween Pumpkin Carving, by Marek Raida</title>
    <script type="text/ecmascript" xlink:href="smil.user.js"/>
    <defs>
        <radialGradient id="back" gradientUnits="objectBoundingBox" cx="0.5" cy="0.5">
            <animate attributeName="cx" values="0.55;0.45;0.45;0.5" dur="0.25s" begin="0" calcMode="discrete" repeatCount="indefinite"/>
            <stop offset="0" stop-color="gold"/>
            <stop offset="0.8" stop-color="black">
                <animate attributeName="offset" values="0.8;1;0.9;0.8" dur="3s" begin="0" repeatCount="indefinite"/>
            </stop>
        </radialGradient>

        <mask id="mask" stroke="white" stroke-linejoin="round" stroke-width="2" fill="none"/>
    </defs>

    <g id="pumpkin">
        <path d="M167,74c6-21,12-43,24-61-6-3-12-5-18-8-3,23-8,46-6,69z" fill="#f06205"/>
        <use xlink:href="#outline" fill="#f86707"/>
        <path d="M165,40c35,15,71,33,91,66,15,23,11,53-3,75-18,30-46,53-70,78,47-20,89-59,99-110,7-31-6-65-32-82-25-18-55-24-85-27z" fill="#ff7316"/>
        <path d="M165,40c38,12,77,27,103,57,17,19,20,48,9,71-18,37-51,64-85,85-4,3-14,8-3,3,45-17,87-52,102-100,10-28,1-61-22-79-29-25-67-33-104-37z" fill="#f36000"/>
        <path d="M167,41c25,39,33,88,21,133-6,30-22,57-33,86,29-24,43-61,50-97,6-35,6-74-14-103-6-8-14-15-24-19z" fill="#ff7316"/>
        <path d="M167,41c30,26,40,68,35,106-6,40-22,78-45,110-3,5,10-8,13-12,27-34,39-79,39-123-1-31-11-67-42-81z" fill="#f36000"/>
        <path d="M164,40c-30,39-46,89-39,138,3,28,14,54,20,81-25-24-34-60-36-93-2-38,5-80,32-110,6-7,14-12,23-16z" fill="#f36000"/>
        <path d="M164,40c-34,27-52,71-50,115,2,36,12,73,31,104-25-24-34-60-36-93-2-38,5-80,32-110,6-7,14-12,23-16z" fill="#ff7316"/>
        <path d="M160,40c-35,15-71,34-88,69-14,29-4,63,11,89,10,19,25,35,34,55-42-23-79-65-81-114-3-33,17-65,46-80,23-13,51-18,78-19z" fill="#f36000"/>
        <path d="M160,40c-36,9-75,21-98,52-19,25-15,60-2,86,13,29,33,54,57,75-42-23-79-65-81-114-3-33,17-65,46-80,23-13,51-18,78-19z" fill="#ff7316"/>
        <path d="M170,41c41-9,88-11,124,14,32,20,44,62,35,97-13,52-60,91-111,102-16,4-32,6-48,6,59,1,121-33,144-89,14-31,10-70-14-94-34-33-85-37-130-36z" fill="#ff7316"/>
        <path d="M173,5c2,11,28,11,12,1-4-1-9-4-12-1z" fill="#d85600"/>
    </g>

    <g fill="url(#back)" pointer-events="none" mask="url(#mask)">
        <path id="outline" d="M170,41c41-9,88-11,124,14,32,20,44,62,35,97-13,52-60,91-111,102-55,13-119,5-163-35-36-33-56-88-37-135,13-32,47-51,80-53,25-1,49,3,72,10z"/>
    </g>
    
    <circle cx="20" cy="20" r="16" stroke="black" stroke-width="3" fill="red" onclick="reset()" title="Reset current carving"/>

    <script>
        var pumpkin = document.getElementById("pumpkin"),
            mask = document.getElementById("mask"),
            carving;

        pumpkin.onmousedown = pumpkin.ontouchstart = function(evt) {
            evt.preventDefault();
            carving = document.createElementNS("http://www.w3.org/2000/svg", "path");
            carving.setAttribute("d", "L" + getCoords(evt));
            mask.appendChild(carving);
        }

        pumpkin.onmousemove = pumpkin.ontouchmove = function(evt) {
            if (!carving) return;
            carving.setAttribute("d", carving.getAttribute("d") + getCoords(evt));
        }

        pumpkin.onmouseup = pumpkin.ontouchend = pumpkin.ontouchcancel = function(evt) {
            if (!carving) return;
            carving.setAttribute("d", carving.getAttribute("d") + " z");
            carving.setAttribute("fill", "white");
            carving = null;
        }

        function getCoords(evt) {
            return (evt.touches) ?
                " " + evt.touches[0].pageX + " " + evt.touches[0].pageY :
                " " + evt.clientX + " " + evt.clientY;
        }
        
        function reset() {
            while (mask.hasChildNodes()) mask.removeChild(mask.firstChild);
        }
    </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: 1

Přehled komentářů

pavel 3D
Zdroj: https://www.zdrojak.cz/?p=13710