Fallstudie – Umblättern-Effekt von 20thingsilearned.com

Hakim El Hattab
Hakim El Hattab

Einführung

2010 haben F-i.com und das Google Chrome-Team gemeinsam an einer HTML5-basierten Bildungs-Web-App namens „20 Dinge, die ich über Browser und das Web gelernt habe“ (www.20thingsilearned.com) gearbeitet. Eine der Hauptideen hinter diesem Projekt war, dass es am besten im Kontext eines Buches präsentiert werden sollte. Da sich der Inhalt des Buches hauptsächlich auf Technologien des offenen Webs bezieht, war es uns wichtig, diesem Ansatz treu zu bleiben. Deshalb ist der Container selbst ein Beispiel dafür, was mit diesen Technologien heute möglich ist.

Buchumschlag und Startseite von „20 Dinge, die ich über Browser und das Web gelernt habe“
Buchcover und Startseite von „20 Dinge, die ich über Browser und das Web gelernt habe“ (www.20thingsilearned.com)

Wir haben uns entschieden, dass das Gefühl eines echten Buches am besten durch die Simulation der positiven Aspekte des analogen Lesens erreicht wird, während gleichzeitig die Vorteile der digitalen Welt in Bereichen wie der Navigation genutzt werden. Viel Aufwand wurde in die grafische und interaktive Darstellung des Leseflusses gesteckt – insbesondere in die Art und Weise, wie die Seiten der Bücher umgeblättert werden.

Erste Schritte

In dieser Anleitung erfahren Sie, wie Sie mit dem Canvas-Element und viel JavaScript einen eigenen Seitenumblätterungseffekt erstellen. Einige grundlegende Codeelemente wie Variablendeklarationen und Ereignis-Listener-Abos wurden aus den Snippets in diesem Artikel entfernt. Sehen Sie sich daher das funktionierende Beispiel an.

Bevor wir loslegen, sollten Sie sich die Demo ansehen, damit Sie wissen, was wir erstellen möchten.

Markieren & Zeichnen

Denken Sie immer daran, dass das, was wir auf dem Canvas zeichnen, nicht von Suchmaschinen indexiert, von Besuchern ausgewählt oder bei Suchanfragen im Browser gefunden werden kann. Aus diesem Grund werden die Inhalte, mit denen wir arbeiten, direkt in das DOM eingefügt und dann mit JavaScript manipuliert, sofern verfügbar. Das dafür erforderliche Markup ist minimal:

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

Wir haben ein Haupt-Containerelement für das Buch, das wiederum die verschiedenen Seiten unseres Buches und das canvas-Element enthält, auf dem wir die umblätterbaren Seiten zeichnen werden. Innerhalb des section-Elements befindet sich ein div-Wrapper für den Inhalt. Damit können wir die Breite der Seite ändern, ohne das Layout des Inhalts zu beeinflussen. Der div hat eine feste Breite und der section ist so eingestellt, dass sein Überlauf ausgeblendet wird. Dadurch wirkt sich die Breite des section als horizontale Maske für den div aus.

Öffnen Sie das Buch.
Dem Buchelement wird ein Hintergrundbild mit der Papiertextur und dem braunen Buchumschlag hinzugefügt.

Logik

Der Code für das Umblättern ist nicht sehr komplex, aber ziemlich umfangreich, da er viele prozedural generierte Grafiken umfasst. Sehen wir uns zuerst die Beschreibung der Konstantenwerte an, die wir im Code verwenden werden.

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;

Die CANVAS_PADDING wird um das Canvas herum hinzugefügt, damit das Papier beim Umblättern über das Buch hinausragt. Einige der hier definierten Konstanten werden auch in CSS festgelegt. Wenn Sie also die Größe des Buchs ändern möchten, müssen Sie auch die Werte dort aktualisieren.

Konstanten
Die konstanten Werte, die im gesamten Code verwendet werden, um die Interaktion zu verfolgen und das Umblättern der Seite zu zeichnen.

Als Nächstes müssen wir für jede Seite ein Umblätterobjekt definieren. Diese werden ständig aktualisiert, wenn wir mit dem Buch interagieren, um den aktuellen Status des Umblätterns widerzuspiegeln.

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

Zuerst müssen wir dafür sorgen, dass die Seiten richtig übereinander liegen. Dazu ordnen wir die Z-Indexe der Abschnittselemente so an, dass die erste Seite oben und die letzte Seite unten ist. Die wichtigsten Eigenschaften der Flip-Objekte sind die Werte progress und target. Damit wird festgelegt, wie weit die Seite aktuell umgeklappt sein soll. „-1“ bedeutet ganz nach links, „0“ die Mitte des Buchs und „+1“ den äußersten rechten Rand des Buchs.

Fortschritt.
Anhand der Fortschritts- und Zielwerte der Lagen wird bestimmt, wo die Falzseite auf einer Skala von −1 bis +1 gezeichnet werden soll.

Nachdem wir für jede Seite ein Flip-Objekt definiert haben, müssen wir die Eingaben der Nutzer erfassen und verwenden, um den Status des Flips zu aktualisieren.

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

Die Funktion mouseMoveHandler aktualisiert das mouse-Objekt, sodass wir immer auf den aktuellen Cursor-Speicherort zugreifen.

In mouseDownHandler prüfen wir zuerst, ob die Maus auf der linken oder rechten Seite gedrückt wurde, damit wir wissen, in welche Richtung wir die Seiten wechseln möchten. Außerdem prüfen wir, ob es in dieser Richtung eine weitere Seite gibt, da wir uns möglicherweise auf der ersten oder letzten Seite befinden. Wenn nach diesen Prüfungen eine gültige Option zum Umdrehen verfügbar ist, setzen wir das dragging-Flag des entsprechenden Umdrehungsobjekts auf true.

Sobald wir die mouseUpHandler erreicht haben, prüfen wir alle flips und sehen nach, ob eine davon als dragging gekennzeichnet wurde und jetzt freigegeben werden sollte. Wenn eine Seite gedreht wird, legen wir den Zielwert so fest, dass die Seite entsprechend der aktuellen Mausposition gedreht wird. Die Seitennummer wird ebenfalls entsprechend aktualisiert.

Rendering

Nachdem wir die meisten Logikfunktionen implementiert haben, sehen wir uns an, wie wir das Papier falten und auf dem Canvas-Element rendern. Der Großteil davon geschieht innerhalb der Funktion render(), die 60 Mal pro Sekunde aufgerufen wird, um den aktuellen Status aller aktiven Flips zu aktualisieren und zu zeichnen.

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

}
}

Bevor wir mit dem Rendern der flips beginnen, setzen wir den Canvas mit der Methode clearRect(x,y,w,h) zurück. Das Löschen des gesamten Canvas kostet viel Leistung. Es wäre viel effizienter, nur die Bereiche zu löschen, auf die wir zeichnen. Damit wir uns in diesem Tutorial auf das Wesentliche konzentrieren können, belassen wir es dabei, den gesamten Canvas zu löschen.

Wenn ein Flip gezogen wird, aktualisieren wir den target-Wert so, dass er der Mausposition entspricht, aber in einer Skala von -1 bis 1 und nicht in tatsächlichen Pixeln. Außerdem erhöhen wir die progress um einen Bruchteil der Entfernung zur target. Dies führt zu einer flüssigen und animierten Wendung, da sie in jedem Frame aktualisiert wird.

Da wir in jedem Frame alle flips durchgehen, müssen wir darauf achten, nur die aktiven neu zu zeichnen. Wenn ein Umblättern nicht sehr nah am Buchrand (innerhalb von 0,3% von BOOK_WIDTH) erfolgt oder als dragging gekennzeichnet ist, gilt es als aktiv.

Nachdem die gesamte Logik implementiert ist, müssen wir die grafische Darstellung einer Karte je nach aktuellem Status zeichnen. Sehen wir uns den ersten Teil der drawFlip(flip)-Funktion an.

// 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';

In diesem Codeabschnitt werden zuerst eine Reihe visueller Variablen berechnet, die wir zum realistischen Zeichnen der Falte benötigen. Der progress-Wert der Falte, die wir zeichnen, spielt hier eine wichtige Rolle, da dort die Falte der Seite erscheinen soll. Um dem Effekt beim Umblättern mehr Tiefe zu verleihen, haben wir das Papier über die oberen und unteren Ränder des Buchs hinausragen lassen. Dieser Effekt ist am stärksten, wenn ein Umblättern nahe am Buchrücken erfolgt.

Wechseln
So sieht der Seitenumbruch aus, wenn die Seite geblättert oder gezogen wird.

Nachdem alle Werte vorbereitet sind, müssen Sie nur noch das Papier zeichnen.

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

Mit der translate(x,y)-Methode der Canvas API wird das Koordinatensystem verschoben, damit wir die Seitenumblätterung zeichnen können, wobei die Oberseite des Buchrückens als Position 0,0 dient. Beachten Sie, dass wir auch die aktuelle Transformationsmatrix des Canvas save() und restore() anwenden müssen, wenn wir mit dem Zeichnen fertig sind.

Übersetzen
Das ist der Punkt, von dem aus wir das Umblättern zeichnen. Der ursprüngliche Punkt 0,0 befindet sich links oben im Bild. Durch die Änderung mithilfe von „translate(x,y)“ vereinfachen wir die Zeichenlogik.

Mit der foldGradient füllen wir die Form des gefalteten Papiers aus, um ihm realistische Highlights und Schatten zu verleihen. Außerdem fügen wir um die Zeichnung auf dem Papier eine sehr dünne Linie hinzu, damit das Papier nicht verschwindet, wenn es vor hellen Hintergründen verwendet wird.

Jetzt müssen wir nur noch die Form des gefalteten Papiers mithilfe der oben definierten Eigenschaften zeichnen. Die linke und rechte Seite des Papiers sind als gerade Linien gezeichnet und die Ober- und Unterseite sind gebogen, um das Gefühl eines gefalteten Papiers zu vermitteln. Die Stärke dieser Papierbiegung wird durch den Wert verticalOutdent bestimmt.

Geschafft! Sie haben jetzt eine voll funktionsfähige Navigation zum Umblättern der Seiten.

Demo für Seitenumblättern

Beim Effekt zum Umblättern von Seiten geht es darum, das richtige interaktive Gefühl zu vermitteln. Bilder davon können das nicht wirklich vermitteln.

Nächste Schritte

Hard-Flip
Die sanfte Seitenwippe in dieser Anleitung wird noch effektiver, wenn sie mit anderen buchähnlichen Funktionen wie einem interaktiven Hardcover kombiniert wird.

Dies ist nur ein Beispiel dafür, was mit HTML5-Funktionen wie dem Canvas-Element möglich ist. Ich empfehle Ihnen, sich das ausgefeiltere Buch anzusehen, aus dem diese Technik stammt: www.20thingsilearned.com. Dort sehen Sie, wie die Seitenübergänge in einer echten Anwendung angewendet werden können und wie leistungsstark sie in Kombination mit anderen HTML5-Funktionen sind.

Verweise