Uno dei siti più belli che sfrutta l'HTML5 è sicuramente il progetto made in Google "20 cose che ho imparato sui browser e il web", un sito che fa del page flip il suo cavallo di battaglia. Aprite il vostro editor preferito e preparatevi a mettere mano sul codice per ricreare questo effetto che permette di simulare lo sfogliare di un libro.

Una volta superato il colpo d'occhio, assolutamente notevole vista l'accuratezza grafica, noterete che l'utente può sfogliare il libro scegliendo due vie, la prima consiste nell'uso dei classici bottoni, l'altra, quella che vi sorprenderà, si scoprirà avvicinando il puntatore del mouse sul bordo destro del libro che, magicamente, inizierà ad alzarsi in modo da invogliare – ed istruire – il lettore a sfogliare il libro pagina dopo pagina.

Navigando il vasto mare di internet avrete notato l'uso del page flip già prima dell'avvento di HTML5, nella maggior parte dei casi l'effetto è realizzato interamente in linguaggio flash, in questo tutorial scopriremo come realizzare un page flip avvalendoci di HTML5 e di JavaScript, non ci riferiremo al sito sopracitato ma una versione simple che trovate all'indirizzo http://www.html5rocks.com/en/tutorials/casestudies/20things_pageflip.html.

La prima cosa che vi consiglio di fare è scaricarvi i sorgenti e darci un'occhiata per capire di cosa stiamo parlando.

Avrete bisogno di:

  • pageflip.js: codice JavaScript che descriverà il comportamento del libro
  • main.css: un foglio di stile che va ad integrare pageflip.js
  • book.png: un file png che rappresenta il nostro libro
  • paper.png: che rappresenta il disegno di una pagina
  • index.html: una pagina HTML5 che riunirà tutto il lavoro fatto

 

Markup

Iniziamo ricordando ai lettori che tutto quello che vive nel canvas non verrà indicizzato dai motori di ricerca, non sarà selezionabile dai navigatori e non verrà trovato dal search interno del browser, fattori da tenere in considerazione durante la progettazione del sito che vorrete fare.

Per ovviare questo problema SEO unfriendly bisognerà cercare di inserire il contenuto direttamente nel DOM e manipolarlo in JavaScript ove possibile.

Iniziamo con i primi stralci di codice da inserire sulla nostra pagina HTML:

<div id="book"><br />    <canvas id="pageflip-canvas"></canvas><br />    <div id="pages"><br />    <section><br />    <div> <!-- Può contenere qualsiasi tipo di contenuto --> </div><br />    </section><br />    <!-- Potete inserire altri tags <section> qui --><br />    </div><br />    </div>

Il contenitore – div – book ha il compito di contenere il libro in tutta la sua grandezza.
Il tag <canvas> ha l'id pageflip-canvas che gestisce l'effetto che simula il sollevamento della pagina.
Come dice il nome stesso pages rappresenta le varie pagine del libro.
Il contenuto viene gestito da un div generico all'interno del tag HTML5 <section>. Siccome il div deve avere una larghezza fissa si usa <section> dandogli l'onere di far mantenere al div la giusta dimensione in orizzontale.

 

Logic

Che ragionamenti ci sono dietro la realizzazione di un page flip di questa tipologia? Come vedrete il codice è molto lungo ma nel contempo abbastanza semplice, il problema maggiore è dato dalla gestione della grafica che viene gestita da valori costanti contenuti nel file JavaScript:

var BOOK_WIDTH = 830;<br />    var BOOK_HEIGHT = 260;<br />    var PAGE_WIDTH = 400;<br />    var PAGE_HEIGHT = 250;<br />    var PAGE_Y = ( BOOK_HEIGHT - PAGE_HEIGHT ) / 2;<br />    var CANVAS_PADDING = 60;

Per capire di cosa stiamo parlando bisogna osservare l'immagine seguente:

Noterete che il libro è diviso in due parti:

  • book comprende l'intero libro che viene gestito in larghezza (width) e altezza (height).
  • page si riferisce alle pagine, anch'esse vengono gestite con i due parametri width e height.
  • CANVAS_PADDING fa riferimento alla cornice del libro in modo da estenderne la grafica nel momento in cui si volta pagina.

Alcune costanti vengono definite anche usando fogli di stile, nel nostro caso vi rimando alla lettura del file main.css.

Il passo successivo è definire un flip object per ogni pagina che dovrà essere costantemente aggiornato in modo da interagire con il libro indicando lo stato attuale della pagina.

// Crea un riferimento ell'elemento contenitore del libro (book)<br />    var book = document.getElementById( "book" );<br />    // Prende un elenco di tutti gli elementi <section> (le pagine) all'interno del book<br />    var pages = book.getElementsByTagName( "section" );<br />    for( var i = 0, len = pages.length; i < len; i++ ) {<br />    pages[i].style.zIndex = len - i;<br />    flips.push( {<br />    progress: 1,<br />    target: 1,<br />    page: pages[i],<br />    dragging: false<br />    });<br />    }

Si inizia ragionando sui livelli, lo z-index degli elementi deve essere settato in modo che la prima pagina sia più in alto della seconda pagina, la seconda pagina sia più alta della terza e così via. Le proprietà più importanti sono progress e target determinato in quale misura sarà piegata la pagina. Il valore -1 indica il bordo sinistro del libro, 0 il punto centrale e +1 è riferito al bordo destro, l'immagine seguente vi chiarirà meglio le idee.

Ora che abbiamo definito un flip object per ogni pagina abbiamo bisogno di controllare le interazioni dell'utente che visita il sito in modo da gestire il flip.

function mouseMoveHandler( event ) {<br />    <br />    // Indica che il libro parte in coordinate 0,0<br />    mouse.x = event.clientX - book.offsetLeft - ( BOOK_WIDTH / 2 );<br />    mouse.y = event.clientY - book.offsetTop;<br />    }<br />    <br />    function mouseDownHandler( event ) {<br />    // Controlla che il puntatore del mouse sia all'interno del libro<br />    if (Math.abs(mouse.x) < PAGE_WIDTH) {<br />    if (mouse.x < 0 && page - 1 >= 0) {<br />    // Indica che siamo nella pagina sinistra in modo da tornare alla pagina precedente<br />    flips[page - 1].dragging = true;<br />    }<br />    else if (mouse.x > 0 && page + 1 < flips.length) {<br />    // Indica che siamo nella parte destra abilitando il dragging (trascinamento) della pagina //corrente<br />    flips[page].dragging = true;<br />    }<br />    }<br />    <br />    // Gestione della selezione del testo<br />    event.preventDefault();<br />    }<br />    <br />    function mouseUpHandler( event ) {<br />    for( var i = 0; i < flips.length; i++ ) {<br />    // Se la pagina viene trascinata parte l'animazione fino a destinazione<br />    if( flips[i].dragging ) {<br />    // Indica la pagina dove andrà a navigare l'utente<br />    if( mouse.x < 0 ) {<br />    flips[i].target = -1;<br />    page = Math.min( page + 1, flips.length );<br />    }<br />    else {<br />    flips[i].target = 1;<br />    page = Math.max( page - 1, 0 );<br />    }<br />    }<br />    <br />    flips[i].dragging = false;<br />    }<br />    }

Dove:

  • mouseMoveHandler aggiorna costantemente la posizione del mouse
  • mouseDownHandler controlla dove viene premuto il mouse, gli eventi cambiano a seconda della pagina in cui si trova il navigatore, di conseguenza cambia anche la direzione di trascinamento. Viene controllata l'esistenza della pagina fino all'arrivo della prima pagina (nella pagina a sinistra) e dell'ultima pagina (per la pagina destra). Se il flip è permesso dopo questi controlli il flag dragging del corrispondente flip object verrà settato con il valore true.
  • mouseUpHandler serve a gestire l'azione dell'utente atta al rilascio della pagina. Quando viene rilasciato un flip il codice controlla la posizione corrente del mouse.

 

Rendering

Il bello di questo tutorial è vedere l'animazione della pagina che viene voltata, l'animazione è gestita dalla funzione render() che viene chiamata 60 volte al secondo in modo da aggiornare e disegnare lo stato attuale di tutti i flip attivi.

function render() {<br />    // Resetta tutti i pixel nel canvas<br />    context.clearRect( 0, 0, canvas.width, canvas.height );<br />    <br />    for( var i = 0, len = flips.length; i < len; i++ ) {<br />    var flip = flips[i];<br />    <br />    if( flip.dragging ) {<br />    flip.target = Math.max( Math.min( mouse.x / PAGE_WIDTH, 1 ), -1 );<br />    }<br />    <br />    // Cancella i progressi verso l'obiettivo<br />    flip.progress += ( flip.target - flip.progress ) * 0.2;<br />    <br />    // Se inizia il flip della pagina o è a metà del libro viene renderizzato<br />    if( flip.dragging || Math.abs( flip.progress ) < 0.997 ) {<br />    drawFlip( flip );<br />    }<br />    <br />    }<br />    }

Prima di iniziare il rendering dei flip viene reimpostato il canvas utilizzando il metodo clearRect (x, y, w, h). Si potrebbe ottimizzare l'algoritmo cancellando solo alcune parti della tela, per semplificare il tutorial e non uscire fuori tema ci accontentiamo di questo metodo.

Se un flip viene trascinato verrò aggiornato il target con un valore corrispondente allla posizione del mouse, non verranno usati i pixel effettivi ma i valori su una scala da 1 -1. Il progress viene incrementato di una frazione della distanza del target in modo da ottenere una progressione regolare del flip con aggionamenti su ogni fotogramma.

Bisogna ridisegnare solo la porzione di flip attiva, il flip viene considerato come non attivo se ci troviamo vicino ai bordi (0.3% del BOOK_WIDTH) o se è contrassegnato come dragging – nel momento in cui viene spostato -, negli altri casi è considerato come attivo.

Ora che tutta la parte logica è stat chiarita ci apprestiamo a disegnare la rappresentazione grafica di un flip a seconda del suo stato attuale. Analizziamo la funzione drawFlip(flip): // Determina la piega/curva della pagina in un range di valori tra 0-1<br />    var strength = 1 - Math.abs( flip.progress );<br />    <br />    // La larghezza della piega<br />    var foldWidth = ( PAGE_WIDTH * 0.5 ) * ( 1 - flip.progress );<br />    <br />    // La posizione sull'asse X della pagina piegata<br />    var foldX = PAGE_WIDTH * flip.progress + foldWidth;<br />    <br />    // La prospettiva al di fuori del libro mentre viene piegata la pagina<br />    var verticalOutdent = 20 * strength;<br />    <br />    // La larghezza massima delle ombre (shadow) utilizzate<br />    var paperShadowWidth = (PAGE_WIDTH*0.5) * Math.max(Math.min(1 - flip.progress, 0.5), 0);<br />    var rightShadowWidth = (PAGE_WIDTH*0.5) * Math.max(Math.min(strength, 0.5), 0);<br />    var leftShadowWidth = (PAGE_WIDTH*0.5) * Math.max(Math.min(strength, 0.5), 0);<br />    <br />    // Maschera la pagina impostanto la sua larghezza con il foldX<br />    flip.page.style.width = Math.max(foldX, 0) + "px";

Nel codice che abbiamo appena descritto si calcolano una serie di variabili per disegnare la piega in modo realistico. Il progress gioca un un ruolo importante poiché indica ove la pagina piegata è visibile, il bordo è un artefizio grafico per aggiungere profondità.

L'ultimo sforzo è rivolto al disegno della carta:

context.save();<br />    context.translate( CANVAS_PADDING + ( BOOK_WIDTH / 2 ), PAGE_Y + CANVAS_PADDING );<br />    <br />    // Disegna un'ombra marcata nel lato sinistro della pagina<br />    context.strokeStyle = 'rgba(0,0,0,'+(0.05 * strength)+')';<br />    context.lineWidth = 30 * strength;<br />    context.beginPath();<br />    context.moveTo(foldX - foldWidth, -verticalOutdent * 0.5);<br />    context.lineTo(foldX - foldWidth, PAGE_HEIGHT + (verticalOutdent * 0.5));<br />    context.stroke();<br />    <br />    // Ombre del lato destro<br />    var rightShadowGradient = context.createLinearGradient(foldX, 0,<br />    foldX + rightShadowWidth, 0);<br />    rightShadowGradient.addColorStop(0, 'rgba(0,0,0,'+(strength*0.2)+')');<br />    rightShadowGradient.addColorStop(0.8, 'rgba(0,0,0,0.0)');<br />    <br />    context.fillStyle = rightShadowGradient;<br />    context.beginPath();<br />    context.moveTo(foldX, 0);<br />    context.lineTo(foldX + rightShadowWidth, 0);<br />    context.lineTo(foldX + rightShadowWidth, PAGE_HEIGHT);<br />    context.lineTo(foldX, PAGE_HEIGHT);<br />    context.fill();<br />    <br />    // Ombre del lato sinistro<br />    var leftShadowGradient = context.createLinearGradient(<br />    foldX - foldWidth - leftShadowWidth, 0, foldX - foldWidth, 0);<br />    leftShadowGradient.addColorStop(0, 'rgba(0,0,0,0.0)');<br />    leftShadowGradient.addColorStop(1, 'rgba(0,0,0,'+(strength*0.15)+')');<br />    <br />    context.fillStyle = leftShadowGradient;<br />    context.beginPath();<br />    context.moveTo(foldX - foldWidth - leftShadowWidth, 0);<br />    context.lineTo(foldX - foldWidth, 0);<br />    context.lineTo(foldX - foldWidth, PAGE_HEIGHT);<br />    context.lineTo(foldX - foldWidth - leftShadowWidth, PAGE_HEIGHT);<br />    context.fill();<br />    <br />    // Il gradiente applicato alla carta piegata (ombre e luci)<br />    var foldGradient = context.createLinearGradient(<br />    foldX - paperShadowWidth, 0, foldX, 0);<br />    foldGradient.addColorStop(0.35, '#fafafa');<br />    foldGradient.addColorStop(0.73, '#eeeeee');<br />    foldGradient.addColorStop(0.9, '#fafafa');<br />    foldGradient.addColorStop(1.0, '#e2e2e2');<br />    <br />    context.fillStyle = foldGradient;<br />    context.strokeStyle = 'rgba(0,0,0,0.06)';<br />    context.lineWidth = 0.5;<br />    <br />    // Disegna parti di carta piegate<br />    context.beginPath();<br />    context.moveTo(foldX, 0);<br />    context.lineTo(foldX, PAGE_HEIGHT);<br />    context.quadraticCurveTo(foldX, PAGE_HEIGHT + (verticalOutdent * 2),<br />    foldX - foldWidth, PAGE_HEIGHT + verticalOutdent);<br />    context.lineTo(foldX - foldWidth, -verticalOutdent);<br />    context.quadraticCurveTo(foldX, -verticalOutdent * 2, foldX, 0);<br />    <br />    context.fill();<br />    context.stroke();<br />    <br />    context.restore();

Nel canvas abbiamo usato le API translate(x,y) per compensare il sistema di co-ordinate, in modo da avere la posizione 0,0 non più in alto a sinistra ma nella metà del libro come potete vedere nell'immagine.

La funzione save () registra le trasformazioni della matrice nel canvas mentre restore () viene richiamata una volta finito il disegno.

Il foldGradient ha il compito di gestire la forma della della carta piegata in modo da avere un effetto realistico con luci e ombre. Viene inoltre aggiunta una sottile linea in modo che la carta non scompaia durante il flip.
I lati sinistro e destro sono disegnati con linee rette mentre i lati superiore ed inferiore sono curvi per dare l'idea di piegatura. L'effetto di carta curvata è determinato dal valore verticalOutdent.

Augurandovi buon lavoro e una facile integrazione nei vostri siti del Page Flip trattato in questo tutorial vi rimando, per domande e feedback, allo spazio per i commenti di HTML5TODAY.

 

Conclusioni e link

Vedi l'esempio di lavoro
Scarica il codice sorgente (75k. Zip)