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

Hakim El Hattab
Hakim El Hattab

Wprowadzenie

W 2010 r. F-i.com i zespół Google Chrome opracowali opartą na HTML5 edukacyjną aplikację internetową o nazwie 20 rzeczy, których nauczyłem się o przeglądarkach i internecie (www.20thingsilearned.com). Jednym z głównych założeń tego projektu było to, że najlepiej będzie go zaprezentować w kontekście książki. Treść książki dotyczy technologii otwartej sieci, dlatego uznaliśmy, że ważne jest, aby pozostać wiernym tej idei i sprawić, by sama książka była przykładem tego, co możemy dziś osiągnąć dzięki tym technologiom.

Okładka książki i strona główna „20 rzeczy, których nauczyłem się o przeglądarkach i internecie”
Okładka i strona główna książki „20 rzeczy, których nauczyłem się o przeglądarkach i internecie” (www.20thingsilearned.com)

Uznaliśmy, że najlepszym sposobem na osiągnięcie wrażenia prawdziwej książki jest symulowanie dobrych aspektów analogowego czytania, przy jednoczesnym wykorzystaniu zalet świata cyfrowego w takich obszarach jak nawigacja. Włożyliśmy dużo wysiłku w opracowanie graficznej i interaktywnej formy czytania, zwłaszcza sposobu, w jaki strony książek są przewracane.

Pierwsze kroki

W tym samouczku pokażemy Ci, jak utworzyć własny efekt przewracania stron za pomocą elementu canvas i kodu JavaScript. Niektóre podstawowe fragmenty kodu, takie jak deklaracje zmiennych i subskrypcje odbiorników zdarzeń, zostały pominięte w przykładach w tym artykule, więc pamiętaj, aby zapoznać się z działającym przykładem.

Zanim zaczniemy, warto obejrzeć wersję demonstracyjną, aby wiedzieć, co chcemy stworzyć.

Znacznik

Pamiętaj, że to, co narysujesz na płótnie, nie może być indeksowane przez wyszukiwarki, wybierane przez odwiedzających ani znajdowane przez wyszukiwania w przeglądarce. Z tego powodu treści, z którymi będziemy pracować, są umieszczane bezpośrednio w DOM, a następnie modyfikowane przez JavaScript, jeśli jest dostępny. Wymagany markup jest minimalny:

<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 książki, który z kolei zawiera różne strony książki i element canvas, na którym będziemy rysować przewracane strony. W elemencie section znajduje się element opakowujący div, który zawiera treść. Jest on potrzebny, aby można było zmieniać szerokość strony bez wpływu na układ jej zawartości. Element div ma stałą szerokość, a element section jest ustawiony tak, aby ukrywać przepełnienie. W efekcie szerokość elementu section działa jak pozioma maska dla elementu div.

Otwórz książkę.
Do elementu książki dodano obraz tła z teksturą papieru i brązową okładką.

Operatory logiczne

Kod wymagany do obsługi przewracania stron nie jest zbyt złożony, ale jest dość obszerny, ponieważ zawiera wiele wygenerowanych proceduralnie grafik. Zacznijmy od opisu stałych wartości, 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;

Wokół obszaru roboczego dodawana jest CANVAS_PADDING, aby podczas przewracania stron papier wystawał poza książkę. Pamiętaj, że niektóre z określonych tu stałych są też ustawione w arkuszu CSS, więc jeśli chcesz zmienić rozmiar książki, musisz też zaktualizować tam wartości.

Stałe.
Stałe wartości używane w kodzie do śledzenia interakcji i rysowania animacji przewracania strony.

Następnie musimy zdefiniować obiekt odwracania dla każdej strony. Będą one stale aktualizowane w miarę interakcji z książką, aby odzwierciedlać bieżący stan odwracania.

// 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 musimy się upewnić, że strony są prawidłowo ułożone, poprzez uporządkowanie kolejności nakładania elementów sekcji tak, aby pierwsza strona była na górze, a ostatnia na dole. Najważniejsze właściwości obiektów do przerzucania to wartości progresstarget. Służą one do określania, jak bardzo strona powinna być obecnie złożona. Wartość -1 oznacza złożenie do lewej krawędzi, 0 – złożenie na środku książki, a +1 – złożenie do prawej krawędzi.

Postęp.
Wartości postępu i docelowe dla przewracania są używane do określania, gdzie na skali od –1 do +1 powinna być narysowana strona składana.

Teraz, gdy mamy zdefiniowany obiekt odwracania dla każdej strony, musimy zacząć rejestrować dane wejściowe użytkowników i używać ich do aktualizowania stanu odwracania.

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, dzięki czemu zawsze pracujemy z najnowszą lokalizacją kursora.

mouseDownHandler sprawdzamy, czy mysz została naciśnięta na lewej czy prawej stronie, aby wiedzieć, w którą stronę zacząć przewracać strony. Sprawdzamy też, czy w tym kierunku istnieje inna strona, ponieważ możemy być na pierwszej lub ostatniej stronie. Jeśli po tych sprawdzeniach dostępna jest prawidłowa opcja odwrócenia, ustawiamy flagę dragging odpowiedniego obiektu odwrócenia na true.

Gdy osiągniemy mouseUpHandler, sprawdzamy wszystkie flips i czy któreś z nich zostały oznaczone jako dragging i powinny zostać opublikowane. Gdy klapka zostanie zwolniona, ustawiamy jej wartość docelową tak, aby odpowiadała stronie, na którą powinna się obrócić w zależności od bieżącego położenia myszy. Numer strony jest też aktualizowany, aby odzwierciedlać tę nawigację.

renderowanie,

Teraz, gdy mamy już większość logiki, pokażemy, jak renderować składany papier na elemencie canvas. Większość tych działań odbywa się w funkcji render(), która jest wywoływana 60 razy na sekundę, aby aktualizować i rysować bieżący stan wszystkich aktywnych obrotó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ć flips, resetujemy obszar rysowania za pomocą metody clearRect(x,y,w,h). Czyszczenie całego obszaru roboczego wiąże się z dużym obciążeniem wydajności, dlatego znacznie wydajniejsze byłoby czyszczenie tylko tych regionów, na których rysujemy. Aby nie odbiegać od tematu tego samouczka, ograniczymy się do wyczyszczenia całego obszaru roboczego.

Jeśli element jest przeciągany, aktualizujemy jego wartość target, aby była zgodna z pozycją myszy, ale w skali od -1 do 1, a nie w rzeczywistych pikselach. Zwiększamy też wartość progress o ułamek odległości do target. Dzięki temu animacja będzie płynna, ponieważ jest aktualizowana w każdej klatce.

Ponieważ w każdej klatce sprawdzamy wszystkie flips, musimy się upewnić, że ponownie rysujemy tylko te, które są aktywne. Jeśli przewrócenie strony nie nastąpiło bardzo blisko krawędzi książki (w odległości 0,3% od BOOK_WIDTH) lub zostało oznaczone jako dragging, jest uznawane za aktywne.

Skoro mamy już całą logikę, musimy narysować graficzną reprezentację odwrócenia w zależności od jego bieżącego 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';

Ta część kodu zaczyna się od obliczenia liczby zmiennych wizualnych, które są potrzebne do realistycznego narysowania zagięcia. Wartość progress rysowanego odwrócenia odgrywa tu dużą rolę, ponieważ to w tym miejscu ma się pojawić załamanie strony. Aby dodać głębi efektowi przewracania stron, sprawiamy, że papier wystaje poza górną i dolną krawędź książki. Efekt ten jest najbardziej widoczny, gdy przewracanie strony jest blisko grzbietu książki.

Obróć
Tak wygląda zagięcie strony podczas jej przewracania lub przeciągania.

Wszystkie wartości są już przygotowane, więc wystarczy tylko wylosować zwycięzcę.

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 API Canvas służy do przesunięcia układu współrzędnych, abyśmy mogli narysować przewracanie strony, a górna część grzbietu książki była pozycją 0,0. Pamiętaj, że musimy też save() bieżącą macierz przekształcenia obszaru rysowania i restore() ją po zakończeniu rysowania.

Tłumacz
To punkt, od którego zaczynamy animację przewracania 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.

Symbol foldGradient oznacza, że wypełnimy nim kształt złożonego papieru, aby nadać mu realistyczne światła i cienie. Dodajemy też bardzo cienką linię wokół rysunku na papierze, aby nie znikał on na jasnym tle.

Teraz wystarczy narysować kształt złożonego papieru, używając właściwości zdefiniowanych powyżej. Lewa i prawa strona papieru są narysowane jako proste linie, a górna i dolna strona są zakrzywione, aby oddać wrażenie zgiętego papieru. Siła zagięcia papieru jest określana przez wartość verticalOutdent.

Znakomicie. Masz już w pełni funkcjonalną nawigację z przewracaniem stron.

Wersja demonstracyjna przewracania stron

Efekt przewracania stron polega na przekazywaniu odpowiedniego wrażenia interaktywności, więc oglądanie zdjęć nie oddaje go w pełni.

Następne kroki

Hard-flip
Miękkie przewracanie stron w tym samouczku staje się jeszcze bardziej przydatne, gdy jest połączone z innymi funkcjami przypominającymi książkę, takimi jak interaktywna twarda okładka.

To tylko jeden z przykładów tego, co można osiągnąć dzięki wykorzystaniu funkcji HTML5, takich jak element canvas. Polecam zapoznać się z bardziej zaawansowaną wersją książki, z której pochodzi ta technika: www.20thingsilearned.com. Zobaczysz tam, jak można wykorzystać efekt przewracania stron w prawdziwej aplikacji i jak potężny staje się on w połączeniu z innymi funkcjami HTML5.

Odniesienia

  • Specyfikacja interfejsu API Canvas