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 eine auf HTML5 basierende Web-App namens „20 Things I Learned about Browsers and the Web“ (www.20thingsilearned.com) entwickelt. Eine der wichtigsten Ideen hinter diesem Projekt war, dass es am besten im Kontext eines Buchs präsentiert werden sollte. Da es im Buch hauptsächlich um Open-Web-Technologien geht, war es uns wichtig, dem treu zu bleiben und den Container selbst als Beispiel dafür zu verwenden, was mit diesen Technologien heute möglich ist.

Buchcover und Startseite von „20 Things I Learned About Browsers and the Web“
Buchcover und Startseite von „20 Things I Learned About Browsers and the Web“ (www.20thingsilearned.com)

Wir haben uns entschieden, dass wir das Gefühl eines echten Buches am besten erreichen, indem wir die guten Aspekte des analogen Lesens simulieren und gleichzeitig die Vorteile der digitalen Welt nutzen, z. B. bei der Navigation. Es wurde viel Aufwand in die grafische und interaktive Gestaltung des Leseflusses gesteckt, insbesondere in die Art und Weise, wie die Seiten der Bücher umgeblättert werden.

Erste Schritte

In dieser Anleitung wird beschrieben, wie Sie mit dem Canvas-Element und viel JavaScript einen eigenen Umblättereffekt erstellen. Einige der rudimentären Codezeilen, z. B. Variablendeklarationen und die Registrierung von Event-Listenern, wurden in den Snippets in diesem Artikel weggelassen. 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

Es ist immer wichtig, daran zu denken, dass das, was wir auf die Leinwand zeichnen, nicht von Suchmaschinen indexiert, von einem Besucher ausgewählt oder durch In-Browser-Suchen 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 es verfügbar ist. Der 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 Hauptcontainerelement für das Buch, das wiederum die verschiedenen Seiten des Buchs und das canvas-Element enthält, auf dem die umblätternden Seiten gezeichnet werden. Innerhalb des section-Elements befindet sich ein div-Wrapper für den Inhalt. Dieser ist erforderlich, damit wir die Breite der Seite ändern können, ohne das Layout des Inhalts zu beeinträchtigen. Das div hat eine feste Breite und für das section ist festgelegt, dass der Überlauf ausgeblendet wird. Dadurch wirkt die Breite des section als horizontale Maske für das div.

„Buch öffnen“
Dem Buchelement wird ein Hintergrundbild mit der Papierstruktur und dem braunen Buchumschlag hinzugefügt.

Logik

Der Code, der für das Umblättern der Seite erforderlich ist, ist nicht sehr komplex, aber ziemlich umfangreich, da er viele prozedural generierte Grafiken umfasst. Sehen wir uns zuerst die Beschreibung der konstanten Werte an, die wir im gesamten 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;

Der CANVAS_PADDING wird um den 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 die Größe des Buchs ändern möchten, müssen Sie also auch die Werte dort aktualisieren.

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

Als Nächstes müssen wir für jede Seite ein Flip-Objekt definieren. Diese werden ständig aktualisiert, wenn wir mit dem Buch interagieren, um den aktuellen Status des Flips 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 geschichtet sind. Dazu organisieren wir die Z-Indexe der Abschnittselemente so, dass die erste Seite oben und die letzte Seite unten ist. Die wichtigsten Eigenschaften der Flip-Objekte sind die Werte progress und target. Diese Werte werden verwendet, um zu bestimmen, wie weit die Seite derzeit gefaltet sein soll. -1 bedeutet ganz links, 0 bedeutet die Mitte des Buchs und +1 bedeutet ganz rechts.

Fortschritt
Die Fortschritts- und Zielwerte der Flips werden verwendet, um zu bestimmen, wo die Faltseite auf einer Skala von –1 bis +1 dargestellt werden soll.

Nachdem wir nun für jede Seite ein Flip-Objekt definiert haben, müssen wir die Nutzereingabe 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 die aktuelle Cursorposition hin arbeiten.

In mouseDownHandler prüfen wir zuerst, ob die Maustaste auf der linken oder rechten Seite gedrückt wurde, damit wir wissen, in welche Richtung wir mit dem Umblättern beginnen möchten. Außerdem prüfen wir, ob in der entsprechenden Richtung eine weitere Seite vorhanden ist, da wir uns möglicherweise auf der ersten oder letzten Seite befinden. Wenn nach diesen Prüfungen eine gültige Flip-Option verfügbar ist, wird das dragging-Flag des entsprechenden Flip-Objekts auf true gesetzt.

Sobald wir die mouseUpHandler erreicht haben, prüfen wir alle flips und sehen nach, ob welche davon als dragging gekennzeichnet wurden und jetzt freigegeben werden sollten. Wenn ein Flip ausgelöst wird, wird der Zielwert so festgelegt, dass er der Seite entspricht, zu der geflippt werden soll, abhängig von der aktuellen Mausposition. Die Seitenzahl wird ebenfalls entsprechend aktualisiert.

Rendering

Nachdem wir nun den Großteil unserer Logik implementiert haben, sehen wir uns an, wie wir das gefaltete Papier im Canvas-Element rendern. Das meiste davon passiert in 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 von flips beginnen, setzen wir den Canvas mit der Methode clearRect(x,y,w,h) zurück. Das Löschen des gesamten Canvas ist mit einem hohen Leistungsaufwand verbunden. Es wäre viel effizienter, nur die Bereiche zu löschen, in denen wir zeichnen. Damit wir in dieser Anleitung beim Thema bleiben, beschränken wir uns darauf, die gesamte Arbeitsfläche zu löschen.

Wenn ein Flip gezogen wird, aktualisieren wir seinen target-Wert entsprechend der Mausposition, jedoch auf einer Skala von -1 bis 1 anstelle von tatsächlichen Pixeln. Außerdem wird progress um einen Bruchteil der Entfernung zu target erhöht. Dadurch wird ein reibungsloser und animierter Übergang des Flips erreicht, da er bei jedem Frame aktualisiert wird.

Da wir alle flips in jedem Frame durchlaufen, müssen wir darauf achten, dass nur die aktiven neu gezeichnet werden. 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 vorhanden ist, müssen wir die grafische Darstellung eines Flips je nach seinem aktuellen 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 Abschnitt des Codes werden zunächst eine Reihe visueller Variablen berechnet, die wir benötigen, um die Faltung realistisch darzustellen. Der progress-Wert des Flips, den wir zeichnen, spielt hier eine große Rolle, da wir dort den Seitenumbruch platzieren möchten. Um dem Umblättereffekt mehr Tiefe zu verleihen, lassen wir das Papier über die oberen und unteren Kanten des Buchs hinausragen. Dieser Effekt ist am stärksten, wenn sich das Umblättern in der Nähe des Buchrückens befindet.

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

Nachdem alle Werte vorbereitet wurden, muss nur noch das Papier gezeichnet werden.

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

Die translate(x,y)-Methode der Canvas API wird verwendet, um das Koordinatensystem zu verschieben, damit wir den Seitenumbruch zeichnen können. Dabei dient die Oberkante des Buchrückens als Position 0,0. Außerdem müssen wir die aktuelle Transformationsmatrix des Canvas save() und restore(), wenn wir mit dem Zeichnen fertig sind.

Übersetzen
Das ist der Punkt, an dem wir die Seite umblättern. Der ursprüngliche Punkt 0,0 befindet sich oben links im Bild. Durch die Änderung über „translate(x,y)“ wird die Zeichenlogik vereinfacht.

Die foldGradient ist das, womit wir die Form des gefalteten Papiers füllen, um ihr realistische Highlights und Schatten zu verleihen. Außerdem fügen wir eine sehr dünne Linie um die Zeichnung auf dem Papier hinzu, damit das Papier nicht verschwindet, wenn es vor helle Hintergründe gelegt wird.

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

Geschafft! Sie haben jetzt eine voll funktionsfähige Blätterfunktion eingerichtet.

Demo für das Umblättern von Seiten

Beim Umblättereffekt geht es darum, das richtige interaktive Gefühl zu vermitteln. Bilder davon werden dem nicht gerecht.

Nächste Schritte

Hard-Flip
Der sanfte Seitenübergang in diesem Tutorial wird noch leistungsfähiger, wenn er 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 erreicht werden kann. Ich empfehle Ihnen, sich das Buch www.20thingsilearned.com anzusehen, aus dem diese Technik stammt. Dort sehen Sie, wie die Seitenübergänge in einer echten Anwendung eingesetzt werden können und wie leistungsstark sie in Kombination mit anderen HTML5-Funktionen sind.

Verweise