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

Hakim El Hattab
Hakim El Hattab

Einleitung

2010 haben F-i.com und das Google Chrome-Team gemeinsam eine HTML5-basierte lehrreiche Web-App namens 20 Dinge, die ich über Browser und das Web gelernt habe (www.20thingsilearned.com) erarbeitet. Eine der Hauptideen hinter diesem Projekt war, dass es am besten im Kontext eines Buchs präsentiert wird. Da es in dem Buch vor allem um offene Webtechnologien geht, waren wir der Ansicht, dass es wichtig ist, diesem treu zu bleiben, indem der Container selbst ein Beispiel dafür ist, was diese Technologien heute ermöglichen.

Buchcover 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 entschieden, dass der beste Weg, den Eindruck eines realen Buches zu erzeugen, darin besteht, die guten Teile des analogen Leseerlebnisses zu simulieren und gleichzeitig die Vorteile der digitalen Welt in Bereichen wie der Navigation zu nutzen. Es wurde viel Aufwand in die grafische und interaktive Darstellung des Leseflusses gesteckt, vor allem, wie die Seiten der Bücher von einer Seite zur anderen blättern.

Erste Schritte

In dieser Anleitung erfahren Sie, wie Sie mit dem Canvas-Element und viel JavaScript einen eigenen Umblättern-Effekt erstellen. Ein Teil des rudimentären Codes wie Variablendeklarationen und Event-Listener-Abos wurden in diesem Artikel nicht in die Snippets aufgenommen. Verweisen Sie also auf das Arbeitsbeispiel.

Bevor wir loslegen, sollten Sie sich die Demo ansehen, damit Sie wissen, was wir entwickeln werden.

Markup

Was wir auf dem Canvas zeichnen, kann nicht von Suchmaschinen indexiert, von einem Besucher ausgewählt oder durch Suchanfragen im Browser gefunden werden. Aus diesem Grund werden die Inhalte, mit denen wir arbeiten, direkt in das DOM eingefügt und dann, sofern verfügbar, mit JavaScript bearbeitet. Dafür ist nur ein minimales Markup erforderlich:

<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 Buchs enthält, sowie das canvas-Element, auf dem wir die umblätternden Seiten zeichnen. Innerhalb des section-Elements befindet sich ein div-Wrapper für den Inhalt – wir benötigen ihn, um die Breite der Seite ändern zu können, ohne das Layout des Inhalts zu beeinflussen. Das div hat eine feste Breite und der section ist so eingestellt, dass der Überlauf ausgeblendet wird. Dies führt dazu, dass die Breite des section als horizontale Maske für div dient.

Offenes Buch.
Dem „book“-Element wird ein Hintergrundbild mit Papiertextur und brauner Buchjacke hinzugefügt.

Logik

Der Code, der für das Umblättern benötigt wird, ist nicht sehr komplex, aber recht umfangreich, da er viele prozedural generierte Grafiken umfasst. Beginnen wir mit der Beschreibung der konstanten Werte, die wir im gesamten Code verwenden.

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;

Das CANVAS_PADDING-Element wird um den Canvas herum hinzugefügt, sodass das Papier beim Umblättern über das Buch hinaus ausgedehnt werden kann. 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 Interaktionen zu verfolgen und den Umblättern-Effekt zu zeichnen

Als Nächstes müssen wir ein Flip-Objekt für jede Seite definieren. Diese Objekte werden während der Interaktion mit dem Buch ständig aktualisiert, 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 angeordnet sind. Dazu organisieren wir die Z-Indexe der „section“-Elemente so, dass die erste Seite oben und die letzte unten ist. Die wichtigsten Eigenschaften der Flip-Objekte sind die Werte progress und target. Damit wird festgelegt, wie weit die Seite derzeit umgeklappt werden soll. -1 bedeutet ganz nach links, 0 bedeutet die Mitte des Buchs und +1 bedeutet ganz rechts.

Fortschritt.
Anhand der Fortschritts- und Zielwerte der Blätter wird festgelegt, an welcher Stelle die umblätternde Seite auf einer Skala von -1 bis +1 dargestellt werden soll.

Da für jede Seite ein Flip-Objekt definiert ist, müssen wir mit dem Erfassen und Verwenden der Nutzereingabe beginnen, um den Status des Umblätterns 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 neueste Cursorposition hinarbeiten.

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

Sobald mouseUpHandler erreicht ist, gehen wir alle flips durch und prüfen, ob eine davon als dragging markiert und jetzt freigegeben werden sollte. Wenn ein Flip-Objekt ausgelöst wird, wird sein Zielwert entsprechend der aktuellen Mausposition auf die Seite gesetzt, in die das Umblättern erfolgen soll. Die Seitennummer wird ebenfalls aktualisiert, um diese Navigation widerzuspiegeln.

Rendering

Jetzt, da der Großteil der Logik vorhanden ist, sehen wir uns an, wie das Faltpapier auf dem Canvas-Element gerendert wird. Das meiste davon geschieht innerhalb der render()-Funktion, 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 das Canvas mithilfe der Methode clearRect(x,y,w,h) zurück. Das Löschen des gesamten Canvas verursacht große Leistungskosten und es wäre viel effizienter, nur die Regionen zu löschen, die wir tatsächlich nutzen. Damit das Thema der Anleitung nicht weitergeht, löschen wir den gesamten Canvas.

Wenn ein Umdrehen gezogen wird, aktualisieren wir seinen target-Wert so, dass er der Mausposition entspricht, allerdings auf einer Skala von -1 bis 1 und nicht mit den tatsächlichen Pixeln. Außerdem erhöhen wir progress um einen Bruchteil der Entfernung zu target. Dies führt zu einem flüssigen und animierten Verlauf des Umblätterns, da es bei jedem Frame aktualisiert wird.

Da wir alle flips in jedem Frame durchlaufen, müssen wir darauf achten, nur die aktiven Elemente neu zu zeichnen. Wenn sich ein Flip nicht sehr nah am Buchrand befindet (innerhalb von 0,3% von BOOK_WIDTH) oder als dragging gekennzeichnet ist, gilt er als aktiv.

Jetzt, da die gesamte Logik vorhanden ist, müssen wir die grafische Darstellung einer Umdrehung je nach ihrem aktuellen Zustand zeichnen. Als Nächstes werfen wir einen Blick auf den ersten Teil der Funktion 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';

Dieser Codeabschnitt beginnt mit der Berechnung einer Reihe visueller Variablen, die wir benötigen, um den „Fold“ auf realistische Weise zu zeichnen. Dabei spielt der progress-Wert des Umdrehens eine große Rolle, da an dieser Stelle der „Fold“ angezeigt werden soll. Um dem Umblättern-Effekt mehr Tiefe zu verleihen, dehnen wir das Papier über den oberen und unteren Rand des Buchs hinaus. Dieser Effekt ist am stärksten, wenn sich das Umblättern nahe am Buchrücken befindet.

Wechseln
So sieht der Falz aus, wenn die Seite umgedreht oder gezogen wird.

Nachdem Sie alle Werte vorbereitet haben, 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();

Die translate(x,y)-Methode der Canvas API wird verwendet, um das Koordinatensystem so zu verschieben, dass beim Umblättern der Seite der obere Punkt als 0,0-Position verwendet wird. Beachten Sie, dass wir auch die aktuelle Transformationsmatrix des Canvas mit save() und restore() zuweisen müssen, wenn wir mit dem Zeichnen fertig sind.

Übersetzer
An diesem Punkt zeichnen wir den Umblättern-Effekt. Der ursprüngliche 0,0-Punkt befindet sich oben links im Bild, aber wenn wir dies über Translate(x,y) ändern, vereinfachen wir die Zeichenlogik.

Mit foldGradient füllen wir die Form des gefalteten Papiers aus, um ihm realistische Hervorhebungen und Schatten zu verleihen. Außerdem wird die Papierzeichnung mit einer sehr dünnen Linie umrandet, damit das Papier auf einem hellen Hintergrund nicht verschwindet.

Jetzt müssen Sie nur noch die Form des gefalteten Papiers mithilfe der oben definierten Eigenschaften zeichnen. Die linke und die rechte Seite des Papiers werden als gerade Linien gezeichnet, und die obere und die untere Seite sind gebogen, um das gebogene Gefühl eines faltbaren Papiers zu vermitteln. Die Stärke dieser Papierkrümmung wird durch den Wert verticalOutdent bestimmt.

Fertig! Sie haben jetzt eine voll funktionsfähige Navigation zum Umblättern.

Demo zum Umblättern von Seiten

Beim Umblättern-Effekt geht es darum, das richtige interaktive Gefühl zu vermitteln. Bilder des Umblättern-Effekts werden dem nicht gerecht.

Nächste Schritte

Hard-Flip
Der weiche Umblättern-Effekt in dieser Anleitung wird in Kombination mit anderen buchähnlichen Funktionen, wie dem interaktiven Hardcover, noch leistungsstärker.

Dies ist nur ein Beispiel für die Umsetzung von HTML5-Funktionen wie dem Canvas-Element. Wir empfehlen Ihnen, einen Blick auf die raffiniertere Bucherfahrung zu werfen, aus der diese Technik stammt: www.20thingsilearned.com. Dort erfahren Sie, wie der Umblättern-Effekt in einer echten Anwendung angewendet werden kann und wie leistungsstark die Methode in Kombination mit anderen HTML5-Funktionen ist.

Verweise