Tutoriál: otáčíme listy v HTML5

Technologie z rodiny moderních webových technologií, označované někdy souhrnně jako „HTML5“, umožňují webdesignérům vytvářet nejrůznější efekty, které byly až donedávna výsadou speciálních aplikací typu FLASH. V článku si ukážeme jeden animační efekt, a to „otáčení listů v knize“.

Úvod

V roce 2010 vytvořily týmy F-i.com a Google Chrome společně výukovou webovou aplikaci v HTML5, kterou nazvali 20 věcí, které jsem se naučil o prohlížečích a webu (20 Things I Learned about Browsers and the Web, dostupné na http://www.20thin­gsilearned.com/). Hlavní myšlenkou tohoto projektu bylo zobrazit obsah v knižní formě. A jelikož je text převážné o webových technologiích, je projekt zároveň jejich ukázkou.

Článek je překladem článku Case Study: Page Flip Effect from 20thingsilear­ned.com z webu html5rocks.com, jehož autorem je Hakim el Hattab, F-i.

Rozhodli jsme se, že pro čtenáře bude nejlepší, když vytvoříme vzhled a chování reálných knih, ale zároveň využijeme všech výhod, které poskytuje elektronický text – například navigace v textu. Dali jsme si hodně záležet na grafickém rozhraní a ovládání, zvláště pak na samotném otáčení stran knihy.

Začínáme

Tento tutoriál ukáže, jak vytvořit efekt otáčení listů knihy s využitím elementu canvas a JavaScriptu. Množství nezbytného kódu, který ale není pro vlastní funkci důležitý (jako například deklarace proměnných či zachytávání událostí) zde vynecháme, takže se nezapomeňte podívat do ukázkového příkladu.

Také je dobré se nejdříve podívat na funkční demo, abyste měli představu, oč se tu vlastně snažíme.

HTML kód

Je nutné si uvědomit, že nic, co vykreslíme do elementu canvas, nemohou vyhledávače indexovat a uživatelé označit nebo vyhledat. Z toho důvodu je veškerý obsah, se kterým budeme pracovat, vložen do DOMu a manipulujeme s ním pomocí JavaScriptu. HTML kód, který k tomu potřebujeme, je minimální:

    <div id="book">
      <canvas id="pageflip-canvas"></canvas>
      <div id="pages">
        <section>
          <div> <!-- Libovolný obsah --> </div>
        </section>
        <!-- Další tagy  -->
      </div>
    </div>

Máme tak jeden hlavní tag s id book pro celou knihu, který obsahuje jednotlivé stránky i samotný canvas, do kterého budeme stránky kreslit. Uvnitř tagu section je také div s pevně nastavenou šířkou, kterým obalujeme vlastní text – to je důležité a umožňuje nám to měnit šířku stránky, aniž bychom ovlivnili její obsah.

Základní funkce

Kód potřebný k efektu otáčení stran není nijak složitý, ale je rozsáhlý, jelikož obsahuje velké množství automaticky generované grafiky. Pojďme se podívat na konstanty, které budeme v našem kódu používat:

    var BOOK_WIDTH = 830;
    var BOOK_HEIGHT = 260;
    var PAGE_WIDTH = 400;
    var PAGE_HEIGHT = 250;
    var PAGE_Y = ( BOOK_HEIGHT - PAGE_HEIGHT ) / 2;
    var CANVAS_PADDING = 60;

Konstanta CANVAS_PADDING přidává okraje okolo elementu canvas. To je důležité zejména během otáčení stránek, kdy se ohyb papíru zobrazuje i mimo pozadí knihy. Také upozorníme, že některé konstanty jsou definované i v CSS, takže pokud je budete chtít změnit, musíte upravit i kaskádové styly.

Dále vytvoříme objekt, který bude obsahovat všechny stránky. Jak bude uživatel s knihou pracovat, bude se tento objekt neustále aktualizovat.

    // Proměnná odkazující na celou knihu.
    var book = document.getElementById( "book" );

    // List všech položek 'section' (stran) v knize.
    var pages = book.getElementsByTagName( "section" );

    for( var i = 0, len = pages.length; i < len; i++ ) {
    pages[i].style.zIndex = len - i;

    flips.push( {
      progress: 1,
      target: 1,
      page: pages[i],
      dragging: false
    });
    }

Každé straně knihy přiřadíme z-index, takže se strany zobrazí ve správném pořadí – první strana úplně nahoře, poslední strana na konci. Další důležité položky jsou progress a target, kterými ovlivňujeme polohu, ve které se každá strana nachází. Hodnota –1 znamená levý okraj knihy, nula je uprostřed a +1 pak pravý okraj.

Nyní, když už máme jednotlivé strany načteny a nastavili jsme jejich z-index a další hodnoty, přidáme kód, který se postará o zpracování uživatelských vstupů.

    function mouseMoveHandler( event ) {
      // Offset pozice myši. Hodnota 0,0 je uprostřed knihy nahoře.
      mouse.x = event.clientX - book.offsetLeft - ( BOOK_WIDTH / 2 );
      mouse.y = event.clientY - book.offsetTop;
    }

    function mouseDownHandler( event ) {
      // Ujistíme se, že kurzor myši je uvnitř knihy.
      if (Math.abs(mouse.x) < PAGE_WIDTH) {
        if (mouse.x < 0 && page - 1 >= 0) {
          // Jsme na levé polovině knihy, otoč předchozí stránku.
          flips[page - 1].dragging = true;
        }
        else if (mouse.x > 0 && page + 1 < flips.length) {
          // Jsme na pravé polovině knihy, otoč na následující stránku.
          flips[page].dragging = true;
        }
      }

      // Zakáže označení textu.
      event.preventDefault();
    }

    function mouseUpHandler( event ) {
      for( var i = 0; i < flips.length; i++ ) {
        // Strana, která má být otočena.
        if( flips[i].dragging ) {
          // Určíme, na kterou stranu budeme otáčet.
          if( mouse.x < 0 ) {
            flips[i].target = -1;
            page = Math.min( page + 1, flips.length );
          }
          else {
            flips[i].target = 1;
            page = Math.max( page - 1, 0 );
          }
        }

        flips[i].dragging = false;
      }
    }

Funkce mouseMoveHandler pravidelně aktualizuje objekt mouse, takže máme vždy aktuální pozici myši absolutně od středu knihy.

V mouseDownHandler  zkontrolujeme, jestli uživatel kliknul myší na levou, nebo pravou stranu knihy a rozhodneme, kterým směrem budeme otáčet. Také se ujistíme, jestli nejsme na začátku, resp. na konci knihy. Poté nastavíme proměnnou dragging na hodnotu true u odpovídající stánky  flips[i].

Jakmile uživatel pustí tlačítko myši, ve funkci mouseUpHandler projdeme všechny stránky flips a najdeme tu, která se má otáčet (má nastavenu proměnnou dragging). Podle aktuální pozice myši nastavíme cílovou pozici stránky pomocí target. Také se aktualizuje číslo strany, na které se nacházíme.

Renderování

Nyní se podíváme na kód, který vykresluje strany během otáčení do elementu canvas. Nejdůležitější je funkce render, která se volá 60krát za vteřinu a překresluje stránky v aktuální pozici.

    function render() {
      // Smaž celé plátno.
      context.clearRect( 0, 0, canvas.width, canvas.height );

      for( var i = 0, len = flips.length; i < len; i++ ) {
        var flip = flips[i];

        if( flip.dragging ) {
          flip.target = Math.max( Math.min( mouse.x / PAGE_WIDTH, 1 ), -1 );
        }

        // Aktualizuj 'progress'.
        flip.progress += ( flip.target - flip.progress ) * 0.2;

        // Pokud je strana právě otáčena, nebo je poblíž středu
        // knihy, vykresli ji.
        if( flip.dragging || Math.abs( flip.progress ) < 0.997 ) {
          drawFlip( flip );
        }
      }
    }

Předtím, než začneme cokoliv vykreslovat, smažeme canvas pomocí funkce clearRect(x,y,w,h). Mazání celé plochy není příliš efektivní, mnohem vhodnější je mazat pouze tu část, která se má překreslovat. Abychom však zachovali tento tutoriál co nejjednodušší, mažeme celou plochu.

Pokud uživatel zrovna stránku otáčí, aktualizujeme target podle aktuální pozice myši (ukládáme relativní pozici –1, nebo +1). Také inkrementujeme proměnnou  progress.

Jelikož procházíme všechny strany knihy zvlášť, musíme se ujistit, že překreslujeme pouze aktuální stranu – to je taková, která je označena jako dragging, nebo není příliš blízko okraje ( progress je alespoň 0,3 % BOOK_WIDTH od okraje).

Teď se podíváme na funkci drawFlip(flip), která se stará o vykreslení otáčené stránky na dané pozici.

    // Urči tloušťku ohybu v rozsahu 0-1.
    var strength = 1 - Math.abs( flip.progress );

    // Šířka ohybu.
    var foldWidth = ( PAGE_WIDTH * 0.5 ) * ( 1 - flip.progress );

    // Pozice ohybu na ose x.
    var foldX = PAGE_WIDTH * flip.progress + foldWidth;

    // Velikost přesahu ohybu přes okraj knihy.
    var verticalOutdent = 20 * strength;

    // Maximální šířka stínů.
    var paperShadowWidth = (PAGE_WIDTH*0.5) * Math.max(Math.min(1 - flip.progress, 0.5), 0);
    var rightShadowWidth = (PAGE_WIDTH*0.5) * Math.max(Math.min(strength, 0.5), 0);
    var leftShadowWidth = (PAGE_WIDTH*0.5) * Math.max(Math.min(strength, 0.5), 0);

    // Vykresli ohyb papíru přes samotnou stranu na pozici 'foldX'.
    flip.page.style.width = Math.max(foldX, 0) + "px";

Tento kód počítá proměnné, podle kterých se vykreslí realisticky vypadající ohyb papíru. Proměnná progress je zde velmi důležitá, jelikož podle ní se ohyb umístí na papíru. Také pomocí ní počítáme přesah ohybu za okraj knihy.

V tomto okamžiku již máme všechno připraveno na samotné kreslení!

    context.save();
    context.translate( CANVAS_PADDING + ( BOOK_WIDTH / 2 ), PAGE_Y + CANVAS_PADDING );

    // Vykreslí ostrý stín na levém okraji papíru.
    context.strokeStyle = 'rgba(0,0,0,'+(0.05 * strength)+')';
    context.lineWidth = 30 * strength;
    context.beginPath();
    context.moveTo(foldX - foldWidth, -verticalOutdent * 0.5);
    context.lineTo(foldX - foldWidth, PAGE_HEIGHT + (verticalOutdent * 0.5));
    context.stroke();

    // Stín na pravé straně.
    var rightShadowGradient = context.createLinearGradient(foldX, 0,
        foldX + rightShadowWidth, 0);
    rightShadowGradient.addColorStop(0, 'rgba(0,0,0,'+(strength*0.2)+')');
    rightShadowGradient.addColorStop(0.8, 'rgba(0,0,0,0.0)');

    context.fillStyle = rightShadowGradient;
    context.beginPath();
    context.moveTo(foldX, 0);
    context.lineTo(foldX + rightShadowWidth, 0);
    context.lineTo(foldX + rightShadowWidth, PAGE_HEIGHT);
    context.lineTo(foldX, PAGE_HEIGHT);
    context.fill();

    // Stín nalevo.
    var leftShadowGradient = context.createLinearGradient(
    foldX - foldWidth - leftShadowWidth, 0, foldX - foldWidth, 0);
    leftShadowGradient.addColorStop(0, 'rgba(0,0,0,0.0)');
    leftShadowGradient.addColorStop(1, 'rgba(0,0,0,'+(strength*0.15)+')');

    context.fillStyle = leftShadowGradient;
    context.beginPath();
    context.moveTo(foldX - foldWidth - leftShadowWidth, 0);
    context.lineTo(foldX - foldWidth, 0);
    context.lineTo(foldX - foldWidth, PAGE_HEIGHT);
    context.lineTo(foldX - foldWidth - leftShadowWidth, PAGE_HEIGHT);
    context.fill();

    // Gradient otáčejícího se papíru (světla a stíny).
    var foldGradient = context.createLinearGradient(
        foldX - paperShadowWidth, 0, foldX, 0);
    foldGradient.addColorStop(0.35, '#fafafa');
    foldGradient.addColorStop(0.73, '#eeeeee');
    foldGradient.addColorStop(0.9, '#fafafa');
    foldGradient.addColorStop(1.0, '#e2e2e2');

    context.fillStyle = foldGradient;
    context.strokeStyle = 'rgba(0,0,0,0.06)';
    context.lineWidth = 0.5;

    // Vykreslí ohnutou část papíru.
    context.beginPath();
    context.moveTo(foldX, 0);
    context.lineTo(foldX, PAGE_HEIGHT);
    context.quadraticCurveTo(foldX, PAGE_HEIGHT + (verticalOutdent * 2),
                             foldX - foldWidth, PAGE_HEIGHT + verticalOutdent);
    context.lineTo(foldX - foldWidth, -verticalOutdent);
    context.quadraticCurveTo(foldX, -verticalOutdent * 2, foldX, 0);

    context.fill();
    context.stroke();

    context.restore();

Funkce translate(x,y), dostupná v canvas API, posune počátek souřadného systému do středu knihy. Předtím ale musíme současnou tranformační matici uložit pomocí save() a na konci vykreslování ji zase obnovit pomocí  restore().

Stíny a světlo definované ve foldGradient dodává ohybu papíru realistický vzhled. Také jsme okolo celé stránky nakreslili tenký obrys, takže je papír viditelný i proti světlému pozadí.

Teď již stačí vykreslit tvar ohybu a papír podle hodnot, které jsme vypočítali výše. Levý a pravý okraj otáčeného papíru jsou rovné úsečky a horní a dolní okraje jsou zakřiveny tak, aby vytvářeli iluzi ohybu. Tloušťka tohoto ohybu je určena pomocí proměnné  verticalOutdent.

A to je vše! Nyní máte plně funkční efekt otáčení stran.

Demo

Nejlepší způsob, jak pochopit funkci tohoto efektu, je podívat se na ukázku. Odkaz níže obsahuje plně funkční demo.

Ukázkové demo efektu

Zdrojové kódy (75k, .zip)

Další kroky

Toto byl pouze jeden příklad toto, čeho můžeme pomocí prvku canvas v HTML5 docílit. Doporučuji podívat se na www.20thingsi­learned.com. Zde můžete vidět tento efekt v reálné aplikaci, společně s dalšími vlastnostmi HTML5.

Reference

Je studentem fakulty elektrotechniky a komunikačních technologií na VUT v Brně. V roce 2004 objevil GNU/Linux a od té doby se zajímá o open-source a programování.

Komentáře: 6

Přehled komentářů

Bohuš Super navod - diky
Petr Re: Super navod - diky
Michal Augustýn nejdřív osahat
Petr Chybka
Oldis navod peknej
Petr Timhle clankem jste mi udelali radost
Zdroj: https://www.zdrojak.cz/?p=3491