Wprowadzenie
W 2010 r. F-i.com i zespół Google Chrome współpracowali nad internetową aplikacją edukacyjną opartą na HTML5 o nazwie 20 Things I Learned about Browsers and the Web (www.20thingsilearned.com). Jednym z kluczowych założeń tego projektu było to, że najlepiej będzie go zaprezentować w kontekście książki. Książka dotyczy głównie otwartych technologii internetowych, dlatego uznaliśmy, że warto pozostać w tym duchu i uczynić z kontenera przykład tego, co te technologie pozwalają nam obecnie osiągnąć.
Uznaliśmy, że najlepszym sposobem na osiągnięcie wrażenia czytania prawdziwej książki jest symulowanie zalet analogowego czytania, jednocześnie korzystając z zalet świata cyfrowego w takich obszarach jak nawigacja. Wiele wysiłku włożyliśmy w to, aby proces czytania był graficzny i interakcyjny, zwłaszcza w sposób, w jaki strony książek przechodzą z jednej w drugą.
Pierwsze kroki
W tym samouczku pokażemy Ci, jak utworzyć własny efekt przewracania strony za pomocą elementu canvas i dużej ilości kodu JavaScript. Niektóre fragmenty kodu, takie jak deklaracje zmiennych i subskrypcja odbiornika zdarzeń, zostały pominięte w fragmentach kodu w tym artykule, więc pamiętaj, aby zapoznać się z przykładem działającego kodu.
Zanim zaczniemy, warto obejrzeć prezentację demonstracyjną, aby dowiedzieć się, czego się spodziewać.
Znacznik
Pamiętaj, że to, co narysujesz na płótnie, nie może być indeksowane przez wyszukiwarki, wybierane przez użytkowników ani znajdowane w wyszukiwarce w przeglądarce. Z tego powodu treści, z którymi będziemy pracować, są umieszczane bezpośrednio w DOM, a potem manipulowane za pomocą JavaScriptu, jeśli jest dostępny. Wymagane jest minimalne oznaczenie:
<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ć strony. Wewnątrz elementu section
znajduje się element div
otaczający zawartość. 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 ukryć jego przepełnienie. W wyniku szerokość elementu section
działa jako pozioma maska dla elementu div
.
Operatory logiczne
Kod potrzebny do przewracania stron nie jest bardzo złożony, ale jest dość obszerny, ponieważ zawiera wiele generowanych 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;
CANVAS_PADDING
jest dodawane wokół obszaru roboczego, aby umożliwić wyginanie papieru poza książkę podczas przewracania stron. Pamiętaj, że niektóre z definiowanych tu stałych są też ustawiane w CSS, więc jeśli chcesz zmienić rozmiar książki, musisz też zaktualizować te wartości.
Następnie musimy zdefiniować obiekt przewracania dla każdej strony. Będzie on stale aktualizowany w miarę interakcji z książką, aby odzwierciedlał bieżący stan przewracania.
// 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ą ułożone prawidłowo. W tym celu zorganizujemy indeksy z sekcji tak, aby pierwsza strona była na górze, a ostatnia – na dole. Najważniejsze właściwości obiektów flip to wartości progress
i target
.
Służy on do określenia, jak daleko strona powinna być złożona. Wartość -1 oznacza, że strona jest złożona do końca w lewo, wartość 0 oznacza środek książki, a wartość +1 oznacza, że strona jest złożona do końca w prawo.
Teraz, gdy mamy zdefiniowany obiekt flip dla każdej strony, musimy zacząć rejestrować dane wejściowe użytkowników i używać ich do aktualizowania stanu flipa.
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 && page - 1 >= 0) {
// We are on the left side, drag the previous page
flips[page - 1].dragging = true;
}
else if (mouse.x > 0 && 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 nad najnowszą lokalizacją kursora.
W mouseDownHandler
najpierw sprawdzamy, czy mysz została naciśnięta w dół po lewej czy po prawej stronie, aby wiedzieć, w jaką stronę chcemy zacząć przewracać strony. Sprawdzamy też, czy istnieje kolejna strona w tym kierunku, ponieważ możemy znajdować się na pierwszej lub ostatniej stronie. Jeśli po tych kontrolach dostępna jest prawidłowa opcja przewracania, ustawiamy flagę dragging
odpowiedniego obiektu przewracania na true
.
Gdy dotrzemy do mouseUpHandler
, sprawdzamy wszystkie flips
, aby sprawdzić, czy któreś z nich nie zostały oznaczone jako dragging
i czy nie powinny zostać odblokowane. Gdy zwolnienie flipa zostanie anulowane, ustawiamy jego wartość docelową tak, aby pasowała do strony, na którą powinien się przewrócić, w zależności od bieżącego położenia myszy.
Numer strony jest też aktualizowany, aby uwzględniał tę nawigację.
renderowanie,
Teraz, gdy większość logiki jest już gotowa, pokażemy, jak wyrenderować składany papier na elemencie kanwy. Większość tych działań odbywa się wewnątrz funkcji render()
, która jest wywoływana 60 razy na sekundę w celu aktualizowania i rysowania bieżącego stanu wszystkich aktywnych przełącznikó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 płótno za pomocą metody clearRect(x,y,w,h)
. Wyczyszczenie całego płótna wiąże się z dużym kosztem wydajności, a o wiele wydajniej jest wyczyścić tylko te obszary, na których rysujemy. Aby nie odbiegać od tematu tego samouczka, zostawimy na razie czyszczenie całego płótna.
Jeśli element jest przeciągany, aktualizujemy jego wartość target
, aby odpowiadała pozycji myszy, ale w skali od -1 do 1, a nie w rzeczywistych pikselach.
Zwiększamy też wartość progress
o niewielką część odległości do target
. Spowoduje to płynne i animowane przewracanie, ponieważ wartość ta jest aktualizowana w każdej klatce.
Ponieważ w każdym ujęciu przechodzimy przez wszystkie flips
, musimy zadbać o to, aby odświeżać tylko te aktywne. Jeśli przewracanie nie jest bardzo bliskie krawędzi książki (w odległości 0,3% od BOOK_WIDTH
) lub jest oznaczone jako dragging
, jest uznawane za aktywne.
Teraz, gdy cała logika jest już gotowa, musimy narysować graficzną reprezentację przełącznika w zależności od jego bieżącego stanu. Pora 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 kilku zmiennych wizualnych, których potrzebujemy do realistycznego narysowania zagięcia. Wartość parametru progress
ma duże znaczenie, ponieważ określa, gdzie ma się wyświetlać linia zagięcia. Aby dodać efekt głębi do przewracania stron, papier wystaje poza górną i dolną krawędź książki. Efekt jest najsilniejszy, gdy przewracanie następuje w pobliżu grzbietu książki.
Teraz, gdy wszystkie wartości są gotowe, pozostaje tylko narysować papier.
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 & 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 służy do przesunięcia systemu współrzędnych, aby można było rysować przewracanie strony, gdy górna krawędź grzbietu ma pozycję 0,0. Pamiętaj, że po zakończeniu rysowania musisz też save()
bieżącą macierz przekształcenia kanwy i restore()
do niej.
foldGradient
wypełni kształt złożonego papieru, aby nadać mu realistyczne światła i cienie. Dodajemy też bardzo cienką linię wokół rysunku, aby nie znikał na jasnym tle.
Teraz wystarczy narysować kształt złożonego papieru, korzystając z definiowanych powyżej właściwości. Lewa i prawa strona papieru są rysowane jako proste linie, a górna i dolna są zaokrąglone, aby nadać papierowi wrażenie złożenia. Siła zginania papieru jest określana przez wartość verticalOutdent
.
Znakomicie. Twoja nawigacja przełączania się między stronami jest teraz w pełni funkcjonalna.
Wersja demonstracyjna przewracania strony
Efekt przewracania strony ma na celu przekazanie odpowiedniego wrażenia interakcji, więc oglądanie zdjęć nie oddaje w pełni jego możliwości.
Następne kroki
To tylko jeden przykład tego, co można osiągnąć, korzystając z funkcji HTML5, takich jak element canvas. Polecam Ci zapoznanie się z bardziej dopracowaną wersją książki, z której pochodzi ta technika (www.20thingsilearned.com). Dowiesz się z niej, jak można stosować przewracanie stron w rzeczywistych aplikacjach oraz jak zwiększyć ich możliwości, łącząc tę technikę z innymi funkcjami HTML5.
Pliki referencyjne
- Specyfikacja interfejsu API Canvas