Introduzione
Nel 2010, F-i.com e il team di Google Chrome hanno collaborato a un'app web educativa basata su HTML5 chiamata 20 Things I Learned about Browsers and the 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 principalmente le tecnologie del web aperto, abbiamo ritenuto importante rimanere fedeli a questo approccio rendendo il contenitore stesso un esempio di ciò che queste tecnologie ci consentono di realizzare oggi.
Abbiamo deciso che il modo migliore per ricreare l'esperienza di lettura di un libro reale è simulare le parti migliori dell'esperienza di lettura analogica, sfruttando al contempo i vantaggi del mondo digitale in aree come la navigazione. È stato fatto un grande sforzo per il trattamento grafico e interattivo del flusso di lettura, in particolare per il passaggio da una pagina all'altra dei libri.
Per iniziare
Questo tutorial ti guiderà nella procedura di creazione del tuo effetto di ribaltamento della pagina utilizzando l'elemento canvas e molto JavaScript. Parte del codice rudimentale, come le dichiarazioni delle variabili e l'iscrizione degli ascoltatori di eventi, è stata omessa dagli snippet in questo articolo, quindi ricordati di fare riferimento all'esempio funzionante.
Prima di iniziare, ti consigliamo di dare un'occhiata alla demo per sapere cosa intendiamo creare.
Segni e linee
È sempre importante ricordare che ciò che disegniamo su Canvas non può essere indicizzato dai motori di ricerca, selezionato da un visitatore o trovato dalle 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 girano. All'interno dell'elemento section
è presente un wrapper div
per i contenuti. Ne abbiamo bisogno 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 gestire il passaggio di pagina non è molto complesso, ma è piuttosto esteso poiché coinvolge molti elementi grafici generati 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 da poter far uscire la carta dal libro quando lo giri. Tieni presente che alcune delle costanti definite qui sono impostate anche in CSS, quindi se vuoi modificare le dimensioni del libro dovrai aggiornare anche i valori in CSS.
A questo punto dobbiamo definire un oggetto di rotazione per ogni pagina, che verrà aggiornato costantemente man mano che interagiamo con il libro per riflettere lo stato corrente della rotazione.
// 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
});
}
Innanzitutto dobbiamo assicurarci che le pagine siano disposte correttamente in livelli, 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 rotazione sono i valori progress
e target
.
Questi valori vengono utilizzati per determinare quanto deve essere attualmente piegata la pagina: -1 indica tutto a sinistra, 0 indica il centro esatto del libro e +1 indica il bordo più a destra del libro.
Ora che abbiamo un oggetto di rotazione definito per ogni pagina, dobbiamo iniziare a acquisire e utilizzare l'input degli utenti per aggiornare lo stato della rotazione.
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 del cursore più recente.
In mouseDownHandler
iniziamo controllando se il mouse è stato premuto sulla pagina sinistra o destra per sapere in quale direzione vogliamo iniziare a girare. 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 rotazione valida, imposta il flag dragging
dell'oggetto di rotazione corrispondente su true
.
Una volta raggiunto il mouseUpHandler
, esaminiamo tutti i flips
e controlliamo se alcuni sono stati segnalati come dragging
e ora dovrebbero essere
rilasciati. Quando viene rilasciato un capovolgimento, impostiamo il relativo valore target in modo che corrisponda al lato a cui deve essere capovolto in base alla posizione corrente del mouse.
Anche il numero di pagina viene aggiornato in base a questa navigazione.
Rendering
Ora che la maggior parte della logica è in atto, vedremo come eseguire il rendering della carta piegata nell'elemento canvas. La maggior parte di queste operazioni avviene all'interno della funzione render()
, che viene chiamata 60 volte al secondo per aggiornare e disegnare lo stato corrente di tutti i flip attivi.
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 a eseguire il rendering di flips
, reimpostamo il canvas utilizzando il metodo clearRect(x,y,w,h)
. La cancellazione dell'intero canvas comporta un notevole calo delle prestazioni e sarebbe molto più efficiente cancellare solo le regioni su cui stiamo disegnando. Per mantenere questo tutorial in tema, ci limiteremo a cancellare l'intero canvas.
Se viene trascinata una rotazione, aggiorniamo il relativo valore target
in modo che corrisponda alla posizione del mouse, ma su una scala da -1 a 1 anziché su pixel effettivi.
Inoltre, aumentiamo progress
di una frazione della distanza rispetto a target
, in modo da ottenere una progressione fluida e animata del capovolgimento, poiché viene aggiornato in ogni frame.
Poiché esaminiamo tutti i flips
in ogni frame, dobbiamo assicurarci di ridisegnare solo quelli attivi. Se un capovolgimento non è molto vicino al bordo del libro (entro lo 0,3% di BOOK_WIDTH
) o se è contrassegnato come dragging
, viene considerato attivo.
Ora che tutta la logica è in atto, dobbiamo disegnare la rappresentazione grafica di una rotazione a seconda del suo stato corrente. È arrivato 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 un numero di variabili visive necessarie per disegnare la piega in modo realistico. Il valore progress
della piega che stiamo disegnando è molto importante, poiché è lì che vogliamo che venga visualizzata la piega della pagina. Per aggiungere profondità all'effetto di ribaltamento della pagina, facciamo in modo che la carta si estenda oltre i bordi superiore e inferiore del libro. Questo effetto è al suo massimo quando un ribaltamento è vicino alla rilegatura del libro.
Ora che tutti i valori sono stati preparati, non resta che disegnare il foglio.
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 capovolgimento di pagina con la parte superiore del dorso che funge da posizione 0,0. Tieni presente che dobbiamo anche save()
la
matrice di trasformazione corrente della tela e restore()
quando abbiamo finito di disegnare.
Con foldGradient
riempiremo la forma della carta piegata per dare risalto e ombreggiature realistiche. Aggiungiamo anche una linea molto sottile intorno al disegno su carta in modo che la carta non scompaia se messa su sfondi chiari.
Ora non resta che disegnare la forma del foglio piegato 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 la sensazione di un foglio piegato. La resistenza di questa piega del foglio è determinata dal valore verticalOutdent
.
È tutto. Ora hai a disposizione una navigazione di ribaltamento delle pagine completamente funzionale.
Demo di ribaltamento di pagina
L'effetto di capovolgimento della pagina ha lo scopo di comunicare la sensazione interattiva giusta, quindi non è sufficiente guardare le immagini per apprezzarlo.
Passaggi successivi
Questo è solo un esempio di cosa è possibile 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 estratto del libro all'indirizzo: www.20thingsilearned.com. Qui vedrai come i cambi di pagina possono essere applicati in un'applicazione reale e quanto diventano potenti se abbinati ad altre funzionalità HTML5.
Riferimenti
- Specifica dell'API Canvas