Introduzione
Nel 2010, F-i.com e il team di Google Chrome hanno collaborato a un'app web didattica basata su HTML5 chiamata 20 cose che ho imparato sui browser e sul web (www.20thingsilearned.com). Una delle idee chiave alla base di questo progetto era che sarebbe stato meglio presentarlo nel contesto di un libro. Poiché i contenuti del libro riguardano in gran parte le tecnologie del web aperto, abbiamo ritenuto importante rimanere fedeli a questo concetto rendendo il contenitore stesso un esempio di ciò che queste tecnologie ci consentono di realizzare oggi.

Abbiamo deciso che il modo migliore per ottenere la sensazione di un libro del mondo reale è simulare gli aspetti positivi dell'esperienza di lettura analogica, sfruttando al contempo i vantaggi del mondo digitale in aree come la navigazione. È stato fatto un grande lavoro per il trattamento grafico e interattivo del flusso di lettura, in particolare per il modo in cui le pagine dei libri vengono girate da una pagina all'altra.
Per iniziare
Questo tutorial ti guiderà nella creazione del tuo effetto di cambio pagina utilizzando l'elemento canvas e molto JavaScript. Alcuni codici rudimentali, come le dichiarazioni di variabili e l'iscrizione al listener di eventi, sono stati omessi dagli snippet di questo articolo, quindi ricorda di fare riferimento all'esempio funzionante.
Prima di iniziare, è consigliabile guardare la demo per sapere cosa vogliamo creare.
Segni e linee
È sempre importante ricordare che ciò che disegniamo sul canvas non può essere indicizzato dai motori di ricerca, selezionato da un visitatore o trovato tramite ricerche nel browser. Per questo motivo, i contenuti con cui lavoreremo vengono inseriti direttamente nel DOM e poi manipolati da JavaScript, se disponibile. Il markup richiesto è minimo:
<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>
Abbiamo un elemento contenitore principale per il libro, che a sua volta contiene
le diverse pagine del libro e l'elemento canvas
su cui disegneremo
le pagine che si sfogliano. All'interno dell'elemento section
è presente un
wrapper div
per i contenuti. Questo ci serve per poter modificare la larghezza
della pagina senza influire sul layout dei contenuti. div
ha una
larghezza fissa e section
è impostato per nascondere il relativo overflow, pertanto
la larghezza di section
funge da maschera orizzontale per div
.

Funzione logica
Il codice necessario per il cambio pagina non è molto complesso, ma è piuttosto esteso in quanto coinvolge molte grafiche generate proceduralmente. Iniziamo esaminando la descrizione dei valori costanti che utilizzeremo nel codice.
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;
Il CANVAS_PADDING
viene aggiunto intorno alla tela in modo che la carta si estenda all'esterno del libro quando viene girata. Tieni presente che alcune
costanti definite qui vengono impostate anche in CSS, quindi se vuoi modificare le
dimensioni del libro, dovrai aggiornare anche i valori.

Successivamente, dobbiamo definire un oggetto di capovolgimento per ogni pagina. Questi oggetti verranno aggiornati costantemente man mano che interagiamo con il libro per riflettere lo stato attuale del capovolgimento.
// 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
});
}
Per prima cosa, dobbiamo assicurarci che le pagine siano disposte correttamente
organizzando gli z-index degli elementi della sezione in modo che la prima
pagina sia in alto e l'ultima in basso. Le proprietà più importanti
degli oggetti di inversione sono i valori progress
e target
.
Questi valori vengono utilizzati per determinare la piega della pagina: -1 indica la piega più a sinistra, 0 indica il centro esatto del libro e +1 indica il bordo più a destra del libro.

Ora che abbiamo definito un oggetto di inversione per ogni pagina, dobbiamo iniziare ad acquisire e utilizzare l'input degli utenti per aggiornare lo stato dell'inversione.
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;
}
}
La funzione mouseMoveHandler
aggiorna l'oggetto mouse
in modo da
lavorare sempre in base alla posizione più recente del cursore.
In mouseDownHandler
, iniziamo controllando se il mouse è stato premuto
sulla pagina di sinistra o di destra, in modo da sapere in quale
direzione vogliamo iniziare a sfogliare. Ci assicuriamo inoltre che
esista un'altra pagina in quella direzione, poiché potremmo trovarci nella prima
o nell'ultima pagina. Se dopo questi controlli è disponibile un'opzione di inversione valida,
impostiamo il flag dragging
dell'oggetto di inversione corrispondente su true
.
Una volta raggiunto il mouseUpHandler
, esaminiamo tutti i flips
e controlliamo se qualcuno di questi è stato segnalato come dragging
e deve ora essere
rilasciato. Quando viene rilasciato un flip, il suo valore target viene impostato in modo che corrisponda
al lato a cui deve passare a seconda della posizione attuale del mouse.
Anche il numero di pagina viene aggiornato in base a questa navigazione.
Rendering
Ora che la maggior parte della logica è a posto, vedremo come
renderizzare la carta piegata sull'elemento canvas. La maggior parte di questo avviene
all'interno della funzione render()
, che viene chiamata 60 volte
al secondo per aggiornare e disegnare lo stato attuale di tutte le schede attive.
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 );
}
}
}
Prima di iniziare il rendering di flips
, reimpostiamo il
canvas utilizzando il metodo clearRect(x,y,w,h)
. Cancellare l'intera tela
comporta un costo elevato in termini di prestazioni e sarebbe molto più efficiente
cancellare solo le regioni su cui stiamo disegnando. Per non uscire
dall'argomento di questo tutorial, ci limiteremo a cancellare l'intera
tela.
Se un ribaltamento viene trascinato, aggiorniamo il valore target
in modo che corrisponda alla
posizione del mouse, ma su una scala da -1 a 1 anziché in pixel effettivi.
Inoltre, incrementiamo progress
di una frazione della distanza da target
, in modo da ottenere una progressione fluida e animata del ribaltamento, poiché viene aggiornato a ogni frame.
Poiché esaminiamo tutti i flips
in ogni frame, dobbiamo assicurarci di ridisegnare solo quelli attivi. Se un'inversione non è
molto vicina al bordo del libro (entro lo 0,3% di BOOK_WIDTH
) o se è
contrassegnata come dragging
, viene considerata attiva.
Ora che tutta la logica è a posto, dobbiamo disegnare la rappresentazione grafica
di un flip a seconda del suo stato attuale. È il momento di
esaminare la prima parte della funzione 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';
Questa sezione del codice inizia calcolando una serie di variabili visive
necessarie per disegnare la piega in modo realistico. Il valore
progress
del ribaltamento che stiamo disegnando svolge un ruolo importante, poiché
è qui che vogliamo che appaia la piega della pagina. Per aggiungere profondità all'effetto
di rotazione della pagina, facciamo in modo che la carta si estenda oltre i bordi
superiore e inferiore del libro. Questo effetto raggiunge il suo culmine quando la rotazione è vicina
al dorso del libro.

Ora che tutti i valori sono preparati, non resta che estrarre il foglietto.
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();
Il metodo translate(x,y)
dell'API Canvas viene utilizzato per compensare il sistema di coordinate in modo da poter disegnare il cambio pagina con la parte superiore del dorso che funge da posizione 0,0. Tieni presente che dobbiamo anche save()
la
matrice di trasformazione corrente del canvas e restore()
quando abbiamo finito di disegnare.

Il foldGradient
è ciò con cui riempiremo la forma della carta piegata
per conferirle luci e ombre realistiche. Aggiungiamo anche una linea molto sottile
intorno al disegno sul foglio in modo che non scompaia quando viene posizionato
su sfondi chiari.
Ora non resta che disegnare la forma della carta piegata utilizzando le proprietà che abbiamo definito sopra. I lati sinistro e destro del foglio sono disegnati
come linee rette, mentre i lati superiore e inferiore sono curvi per dare l'impressione
di un foglio piegato. La resistenza di questa piega della carta è
determinata dal valore verticalOutdent
.
È tutto. Ora hai una navigazione a scorrimento di pagina completamente funzionante.
Demo di cambio pagina
L'effetto di cambio pagina serve a comunicare la giusta sensazione interattiva, quindi guardare le immagini non rende giustizia.
Passaggi successivi

Questo è solo un esempio di ciò che si può ottenere utilizzando le funzionalità HTML5 come l'elemento canvas. Ti consiglio di dare un'occhiata all'esperienza di lettura più raffinata da cui è tratto questo esempio all'indirizzo: www.20thingsilearned.com. Qui vedrai come le pagine possono essere sfogliate in un'applicazione reale e quanto diventa potente se abbinata ad altre funzionalità HTML5.
Riferimenti
- Specifica dell'API Canvas