Case study - Effetto volta pagina di 20thingsilearned.com

Hakim El Hattab
Hakim El Hattab

Introduzione

Nel 2010, F-i.com e il team di Google Chrome hanno collaborato alla creazione di un'app web didattica 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 presentato al meglio nel contesto di un libro. Poiché i contenuti del libro riguardano molto le tecnologie del web aperto, abbiamo pensato che fosse importante restare fedeli a questo concetto facendo del container stesso un esempio di ciò che queste tecnologie ci permettono di oggi.

Copertina del libro e home page di "20 cose che ho imparato sui browser e sul web"
Copertina e home page del libro "20 cose che ho imparato sui browser e sul web" (www.20thingsilearned.com)

Abbiamo deciso che il modo migliore per avere la sensazione di un libro reale è simulare le parti positive dell'esperienza di lettura analogica pur sfruttando i vantaggi del regno digitale in aree come la navigazione. Il trattamento grafico e interattivo del flusso di lettura ha richiesto molto impegno, in particolare il modo in cui le pagine dei libri passano da una pagina all'altra.

Per iniziare

Questo tutorial ti guiderà nella creazione di un effetto page capovolto utilizzando l'elemento canvas e molto JavaScript. Parte del codice rudimentale, come le dichiarazioni delle variabili e l'abbonamento al listener di eventi, è stata omessa dagli snippet in questo articolo. Pertanto, ricordati di fare riferimento all'esempio funzionante.

Prima di iniziare, ti consigliamo di guardare la demo in modo da sapere cosa puntiamo a realizzare.

Markup

È 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 tracceremo le pagine capovolte. All'interno dell'elemento section è presente un wrapper div per i contenuti, che deve essere in grado di modificare la larghezza della pagina senza influire sul layout dei contenuti. L'elemento div ha una larghezza fissa e la proprietà section è impostata per nascondere l'overflow, il che si traduce in la larghezza della section che agisce come una maschera orizzontale per div.

Libro aperto.
All'elemento libro viene aggiunta un'immagine di sfondo contenente la texture della carta e la giacca marrone libro.

Funzione logica

Il codice necessario per capovolgere la pagina non è molto complesso, ma è piuttosto ampio poiché prevede molte immagini 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;

Viene aggiunto CANVAS_PADDING intorno alla tela in modo che la carta si estenda all'esterno del libro durante la rotazione. Tieni presente che alcune costanti definite qui sono impostate anche in CSS, quindi se vuoi modificare le dimensioni del libro dovrai anche aggiornare i valori lì.

Costanti.
I valori costanti utilizzati nel codice per monitorare l'interazione e disegnare il capovolgimento di pagina.

Successivamente dobbiamo definire un oggetto flip per ogni pagina, che verranno costantemente aggiornati man mano che interagiamo con il libro per riflettere lo stato attuale della 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 i livelli delle pagine siano corretti 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 capovolti sono i valori progress e target. Questi parametri vengono utilizzati per determinare fino a che punto la pagina deve essere piegata al momento, -1 indica completamente a sinistra, 0 indica il punto morto del libro e +1 indica il bordo più a destra del libro.

Avanzamento.
I valori target e l'avanzamento delle capovolgimenti vengono utilizzati per determinare dove dovrebbe essere disegnata la pagina pieghevole su una scala da -1 a +1.

Ora che abbiamo definito un oggetto flip per ogni pagina, dobbiamo iniziare ad acquisire e utilizzare l'input dell'utente per aggiornare lo stato del capovolgimento.

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;
}
}

La funzione mouseMoveHandler aggiorna l'oggetto mouse in modo da funzionare sempre verso la posizione più recente del cursore.

In mouseDownHandler, iniziamo controllando se il mouse è stato premuto sulla pagina destra o sinistra, in modo da sapere in quale direzione vuoi iniziare a puntare. Ci assicuriamo inoltre che esista un'altra pagina in quella direzione dato che potremmo essere nella prima o nell'ultima pagina. Se dopo questi controlli è disponibile un'opzione di capovolgimento valida, impostiamo il flag dragging dell'oggetto flip corrispondente su true.

Una volta raggiunto il mouseUpHandler, esaminiamo tutti i flips e controlliamo se sono stati segnalati come dragging e ora devono essere rilasciati. Quando viene rilasciato un capovolgimento, impostiamo il valore target in modo che corrisponda al lato a cui deve essere capovolto, a seconda della posizione attuale del mouse. Anche il numero di pagina viene aggiornato per riflettere questa navigazione.

Rendering

Ora che la logica è pronta, vediamo come eseguire il rendering della carta pieghevole sull'elemento canvas. La maggior parte di questo avviene all'interno della funzione render(), che viene chiamata 60 volte al secondo per aggiornare e tracciare lo stato attuale di tutti i capovolgimenti 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 il rendering di flips, reimpostiamo la tela utilizzando il metodo clearRect(x,y,w,h). La pulizia dell'intero canvas comporta una grande spesa in termini di prestazioni e sarebbe molto più efficiente svuotare solo le aree geografiche di cui ci serviamo. Per garantire l'argomento del tutorial, lasciamo che sia tutto cancellato.

Se una capovolgimento viene trascinata, aggiorniamo il relativo valore target in modo che corrisponda alla posizione del mouse, ma in una scala da -1 a 1 anziché in base ai pixel effettivi. Ampliamo inoltre progress di una frazione della distanza rispetto a target, in modo da produrre un avanzamento fluido e animato del capovolgimento poiché si aggiorna a ogni fotogramma.

Poiché esamineremo tutti i flips in ogni frame, dobbiamo assicurarci di ridisegnare solo quelli attivi. Se una rotazione non è molto vicina al bordo del libro (entro lo 0,3% di BOOK_WIDTH) o se viene segnalata come dragging, viene considerata attiva.

Ora che tutta la logica è attiva, dobbiamo tracciare la rappresentazione grafica di un'inversione di tendenza in base al suo stato attuale. È il momento di osservare 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 che dobbiamo tracciare in modo realistico. Il valore progress della capovolgimento che stiamo tracciando gioca un ruolo importante in questo caso, poiché è il punto in cui vogliamo che venga visualizzata la pagina. Per aggiungere profondità all'effetto di capovolgimento di pagina, facciamo in modo che la carta si estenda oltre i bordi superiore e inferiore del libro. Questo effetto raggiunge il culmine quando una volta si avvicina il dorso del libro.

Capovolgi
Questo è l'aspetto del piegatura della pagina quando la pagina viene capovolta o trascinata.

Ora che tutti i valori sono preparati, non ti resta che disegnare la carta.

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();

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 e fare in modo che la parte superiore del dorso funge da posizione 0,0. Tieni presente che dobbiamo anche save() l'attuale matrice di trasformazione della tela e restore() quando abbiamo finito di disegnare.

Traduci
Questo è il punto da cui traiamo il capovolgimento di pagina. Il punto originale di 0,0 si trova in alto a sinistra nell'immagine, ma modificandolo tramite Traduttore(x,y), semplifichiamo la logica del disegno.

Riempiremo la forma della carta piegata con foldGradient per ottenere alte luci e ombre realistiche. Aggiungiamo anche una linea molto sottile intorno al disegno della carta in modo che la carta non scompaia quando viene posizionata su uno sfondo chiaro.

A questo punto non resta che disegnare la forma della carta piegata utilizzando le proprietà descritte in precedenza. I lati sinistro e destro del foglio sono disegnati come linee rette e i lati superiore e inferiore sono curvi per creare la sensazione di piegatura di un foglio di carta. La forza di questa piega della carta è determinata dal valore verticalOutdent.

È tutto. Ora hai a disposizione una navigazione a capovolgimento completamente funzionante.

Demo page Flip

L'effetto voltare pagina consiste nel comunicare il giusto senso interattivo, quindi guardare le immagini non rende esattamente giustizia.

Passaggi successivi

Rigido
La possibilità di girare le pagine in questo tutorial diventa ancora più efficace se abbinata ad altre funzionalità simili a quelle di un libro, come la copertina rigida interattiva.

Questo è solo un esempio di ciò che si può ottenere utilizzando funzionalità HTML5 come l'elemento canvas. Ti consiglio di dare un'occhiata ai libri più raffinati da cui questa tecnica è un estratto sul sito: www.20thingsilearned.com. Potrai scoprire come è possibile applicare la rotazione delle pagine in un'applicazione reale e come diventa efficace se abbinata ad altre funzionalità HTML5.

Riferimenti