Studium przypadku – Efekt odwrócenia strony z witryny 20thingsilearned.com

Hakim El Hattab
Hakim El Hattab

Wstęp

W 2010 r. zespół F-i.com i zespół Google Chrome wspólnie stworzyli edukacyjną aplikację internetową w języku HTML5 zatytułowaną „20 Things I Learned about Browsers and the Web” (www.20thingsilearned.com). Jedną z głównych koncepcji projektu było to, aby prezentował się najlepiej w kontekście książki. Ponieważ treść tej książki w dużej mierze dotyczy otwartych technologii internetowych, uznaliśmy, że ważne jest zachowanie zgodności z tym, że kontener powinien być przykładem tego, co pozwalają nam osiągnąć obecnie dzięki tym technologiom.

Okładka i strona główna książki „Dwadzieścia rzeczy, które należy wiedzieć o przeglądarkach i internecie”
Okładka i strona główna książki „20 rzeczy, które należy wiedzieć o przeglądarkach i internecie” (www.20thingsilearned.com)

Uznaliśmy, że najlepszym sposobem, by poczuć atmosferę rzeczywistej książki, jest symulowanie dobrych elementów analogicznych elementów czytania, a jednocześnie wykorzystanie zalet sfery cyfrowej w takich obszarach jak nawigacja. Wiele wysiłku włożyłeś w graficzną i interaktywną grafikę czytania, a zwłaszcza sposób przechodzenia między stronami książki.

Pierwsze kroki

W tym samouczku pokazujemy, jak utworzyć własny efekt odwracania strony przy użyciu elementu canvas i dużej ilości kodu JavaScript. Część kodu podstawowego, np. deklaracje zmiennych i subskrypcja detektora zdarzeń, została pominięta we fragmentach kodu dostępnych w tym artykule, dlatego pamiętaj, by odwołać się do działającego przykładu.

Zanim zaczniemy, warto obejrzeć prezentację, aby dowiedzieć się, co chcemy stworzyć.

Markup

Pamiętaj, że to, co rysujemy na płótnie, nie może zostać zindeksowane przez wyszukiwarki, wybrane przez użytkownika ani znalezione podczas wyszukiwania w przeglądarce. Dlatego treści, nad którymi będziemy pracować, są umieszczane bezpośrednio w elemencie DOM, a jeśli są dostępne, są modyfikowane za pomocą JavaScriptu. Znaczniki te są nieznaczne:

<div id='book'>
<canvas id='pageflip-canvas'></canvas>
<div id='pages'>
<section>
    <div> <!-- Any type of contents here --> </div>
</section>
<!-- More <section>s here -->
</div>
</div>

Mamy jeden główny element kontenera, który z kolei zawiera różne strony książki i element canvas, na którym rysujemy przewracane strony. Wewnątrz elementu section znajduje się kod div dotyczący treści. Jest on potrzebny do zmiany szerokości strony bez wpływu na układ jej zawartości. Element div ma stałą szerokość, a element section jest ustawiony tak, aby ukrywał niedo końca. W efekcie szerokość elementu section staje się maską poziomą dla elementu div.

Otwórz książkę.
Do elementu książki zostaje dodany obraz tła zawierający teksturę papieru i brązową kurtkę z książką.

Operatory logiczne

Kod wymagany do odwracania strony nie jest zbyt złożony, ale jest dość obszerny, ponieważ zawiera dużo proceduralnie generowanych grafik. Zacznijmy od opisania wartości stałych, których będziemy używać w kodzie.

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;

Element CANVAS_PADDING jest dodawany wokół obszaru roboczego, dzięki czemu papier wychodzi poza książkę podczas odwracania. Niektóre stałe zdefiniowane tutaj są też ustawiane w CSS, więc jeśli chcesz zmienić rozmiar książki, musisz tam też zaktualizować wartości.

Stałe.
Wartości stałe używane w całym kodzie do śledzenia interakcji i rysowania przewrócenia strony.

Następnie musimy zdefiniować obiekt przewrócenia dla każdej strony. Będą one stale aktualizowane w miarę interakcji z książką, aby odzwierciedlić bieżący stan przewinięcia.

// Create a reference to the book container element
var book = document.getElementById( 'book' );

// Grab a list of all section elements (pages) within the book
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
});
}

Najpierw trzeba sprawdzić, czy strony są prawidłowo nakładane, i ułóż kolejność nakładania elementów sekcji w taki sposób, aby pierwsza strona znajdowała się na górze, a ostatnia na dole. Najważniejsze właściwości obiektów odwrócenia to wartości progress i target. Służą one do określania, jak daleko należy złożyć stronę. -1 oznacza do końca w lewo, 0 oznacza środkową część książki, a +1 oznacza jej prawą, skrajną krawędź.

Postęp.
Informacje o postępie i wartościach docelowych są używane do określenia miejsca, w którym należy narysować składaną stronę w skali od -1 do +1.

Po zdefiniowaniu obiektu flip dla każdej strony musimy zacząć przechwytywać dane wejściowe użytkownika i korzystać z nich, aby aktualizować stan przejścia.

function mouseMoveHandler( event ) {
// Offset mouse position so that the top of the book spine is 0,0
mouse.x = event.clientX - book.offsetLeft - ( BOOK_WIDTH / 2 );
mouse.y = event.clientY - book.offsetTop;
}

function mouseDownHandler( event ) {
// Make sure the mouse pointer is inside of the book
if (Math.abs(mouse.x) < PAGE_WIDTH) {
if (mouse.x < 0 &amp;&amp; page - 1 >= 0) {
    // We are on the left side, drag the previous page
    flips[page - 1].dragging = true;
}
else if (mouse.x > 0 &amp;&amp; page + 1 < flips.length) {
    // We are on the right side, drag the current page
    flips[page].dragging = true;
}
}

// Prevents the text selection
event.preventDefault();
}

function mouseUpHandler( event ) {
for( var i = 0; i < flips.length; i++ ) {
// If this flip was being dragged, animate to its destination
if( flips[i].dragging ) {
    // Figure out which page we should navigate to
    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;
}
}

Funkcja mouseMoveHandler aktualizuje obiekt mouse, tak aby zawsze pracował w najnowszym położeniu kursora.

W mouseDownHandler zaczynamy od sprawdzenia, czy przycisk myszy został naciśnięty po lewej czy w prawej stronie, by dowiedzieć się, w którym kierunku mamy zacząć przewracać. Zapewniamy też, że w tym kierunku znajduje się inna strona, ponieważ możemy być na pierwszej lub ostatniej stronie. Jeśli po sprawdzeniu jest dostępna prawidłowa opcja odwrócenia, ustawiamy flagę dragging odpowiedniego obiektu flip na true.

Gdy dotrzemy do obszaru mouseUpHandler, sprawdzimy, czy któreś z nich (flips) nie zostały oznaczone jako dragging i powinny zostać zwolnione. Po zwolnieniu odwrócenia ustawiamy jego wartość docelową tak, aby pasowała do strony, na którą ma się odwrócić w zależności od bieżącej pozycji myszy. Numer strony jest również aktualizowany, aby odzwierciedlać tę nawigację.

renderowanie,

Teraz, gdy większość naszych logiki jest już gotowa, zajmijmy się renderowaniem składanego papieru na elemencie canvas. Większość tego procesu odbywa się w funkcji render(), która jest wywoływana 60 razy na sekundę w celu aktualizowania i rysowania bieżącego stanu wszystkich aktywnych odwrótów.

function render() {
// Reset all pixels in the canvas
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 );
}

// Ease progress towards the target value
flip.progress += ( flip.target - flip.progress ) * 0.2;

// If the flip is being dragged or is somewhere in the middle
// of the book, render it
if( flip.dragging || Math.abs( flip.progress ) < 0.997 ) {
    drawFlip( flip );
}

}
}

Zanim zaczniemy renderować obiekt flips, resetujemy go, korzystając z metody clearRect(x,y,w,h). Wyczyszczenie całej zawartości obszaru roboczego wiąże się z dużym kosztem wydajności i o wiele skuteczniejszym rozwiązaniem jest wyczyszczenie tylko tych regionów, które pokazujemy. Żeby nie przekroczyć tematu, oczyścimy całą kanwę.

Jeśli przeciągnięcie jest przeciągnięte, aktualizujemy jego wartość target, aby pasowała do położenia myszy, ale w skali -1 do 1, a nie w rzeczywistych pikselach. Dodatkowo zwiększamy wartość progress o ułamek odległości do obiektu target. Zapewnia to płynne i animowane odwracanie, ponieważ jest aktualizowany w każdej klatce.

Wszystkie flips sprawdzamy w każdej klatce, więc musimy mieć pewność, że ponownie rysujemy tylko aktywne. Jeśli odwrócenie nie znajduje się bardzo blisko krawędzi książki (w przedziale 0,3% BOOK_WIDTH) lub jest oznaczone jako dragging, jest uznawane za aktywne.

Teraz gdy logika jest już uporządkowana, musimy narysować graficzną reprezentację odwrócenia w zależności od jego aktualnego stanu. Czas przyjrzeć się pierwszej części funkcji drawFlip(flip).

// Determines the strength of the fold/bend on a 0-1 range
var strength = 1 - Math.abs( flip.progress );

// Width of the folded paper
var foldWidth = ( PAGE_WIDTH * 0.5 ) * ( 1 - flip.progress );

// X position of the folded paper
var foldX = PAGE_WIDTH * flip.progress + foldWidth;

// How far outside of the book the paper is bent due to perspective
var verticalOutdent = 20 * strength;

// The maximum widths of the three shadows used
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);

// Mask the page by setting its width to match the foldX
flip.page.style.width = Math.max(foldX, 0) + 'px';

W tej części kodu obliczamy liczbę zmiennych wizualnych, które są potrzebne do narysowania części strony w realistyczny sposób. Duża wartość progress przesunięcia, które rysujemy, ma tutaj duże znaczenie, ponieważ to właśnie tam ma pojawić się część strony widoczna na ekranie. Aby uzyskać efekt przewrócenia strony, wystawamy papier poza górne i dolne krawędzie książki. Efekt jest najsilniejszy, gdy odwrócenie strony znajduje się blisko grzbietu książki.

Odwróć
Tak wygląda strona widoczna po przewinięciu, gdy jest przewracana lub przeciągana.

Teraz, gdy wszystkie wartości są gotowe, pozostaje tylko rysowanie papieru.

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

// Draw a sharp shadow on the left side of the page
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();

// Right side drop shadow
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();

// Left side drop shadow
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 applied to the folded paper (highlights &amp; shadows)
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;

// Draw the folded piece of paper
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();

Metoda translate(x,y) interfejsu Canvas API jest używana do przesunięcia systemu współrzędnych, co pozwala nam narysować odwrócenie strony, gdzie górna część grzbietu funkcjonuje w pozycji 0,0. Pamiętaj, że gdy zakończymy rysowanie, musimy też save() wybrać bieżącą macierz obszaru roboczego i użyć restore().

Tłumacz
W tym miejscu rozpoczynamy odwracanie strony. Oryginalny punkt 0,0 znajduje się w lewym górnym rogu obrazu, ale zmieniając go,za pomocą funkcji Translate(x,y) upraszczamy logikę rysowania.

foldGradient to kształt zwiniętego papieru wypełniamy kształtem zwiniętego papieru, aby zapewnić realistyczne podświetlenia i cienie. Dodajemy też bardzo cienką linię wokół rysunków na papierze, aby papier nie znikał na jasnym tle.

Teraz rysuj kształt zwiniętego papieru za pomocą zdefiniowanych powyżej właściwości. Lewa i prawa strona kartki rysujemy jako proste linie, a górna i dolna strona jest zakrzywiona, aby stworzyć wrażenie wygiętego papieru. Siła zagięcia papieru jest określana przez wartość verticalOutdent.

Znakomicie. W ten sposób można przejść do w pełni funkcjonalnej nawigacji przewracanej strony.

Wersja demonstracyjna przewracania strony

Efekt odwrócenia strony polega na przekazaniu właściwego odczucia interaktywnego, więc samo patrzenie na obrazy nie wystarcza.

Dalsze kroki

Mocne odwrócenie
Miękkie przewrócenie strony w tym samouczku jest jeszcze skuteczniejsze w połączeniu z innymi funkcjami podobnymi do książek, takimi jak interaktywna w twardej oprawie.

To tylko jeden przykład tego, co można osiągnąć przy użyciu funkcji HTML5, takich jak element canvas. Zachęcam do zapoznania się z bardziej dopracowaną wersją tej techniki na stronie www.20thingsilearned.com. Dowiesz się tam, jak można zastosować przewracanie strony w prawdziwej aplikacji i jak skuteczne jest korzystanie z niej w połączeniu z innymi funkcjami HTML5.

Źródła

  • Specyfikacja interfejsu API Canvas