Přejít k navigační liště

Zdroják » Webdesign » Tutoriál: otáčíme listy v HTML5

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

Články Webdesign

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

Komentáře

Subscribe
Upozornit na
guest
6 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
Bohuš

Dekuji za super navod, neco podobneho uz jsem hledal delsi dobu.

Chtel bych se dal zeptat:
1) jak upravit kod tak, aby bylo možné text stranky označit vybrat a kopírovat,
2) jak psat na obe strany knihy a
3) jak upravit vzhled knihy tak, aby pri otaceni listu na jedne strane knihy listy pribyvaly a na druhe ubyvaly (iluze, ze se blizim ke konci knihy) tak, jak to maji na zminene adrese http://www.20thingsilearned.com/

Dekuji za pripadne rady.

Petr

JEdnuduse se zeptejte Hakima – bude mi radost. JE to mladej kluk ktery ted pracuje v USA a delaji tam uzasne veci. Jinak hakim.se mluvi za vse. Zkuste se ho zeptat naprimo. Ma tam i kontakt na twitter.

Michal Augustýn

Doporučuju před čtením článku si tu knihu na http://www.20thingsilearned.com osahat. Ona to totiž není klasická kniha, ale kniha, kde je levá stránka prázdná, resp. listy jsou potištěné jen z jedné strany, což situaci dost zjednodušuje…

Petr

Je tam reprodukovatelna chyba – pokud otocim stranku a mysi skoncim v tom hnedem uzkem obdelnicku vlevo a tam tu stranku pustim, tak se efekt pro dalsi otaceni poskodi – neni videt otacejici se papir.

FF 4.0

Oldis

myslenka dobra, zpracovani taky, jen se to tluce s gestama pro stranku vpred a vzad ;)

Petr

parada.

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.