Fallstudie: Umblättern-Effekt aus 20thingsilearned.com

HTML5 Rocks

Einführung

Im Jahr 2010 haben F-i.com und das Google Chrome-Team gemeinsam eine HTML5-basierte, informative Web-App mit dem Namen "20 Dinge, die ich über Browser und das Web gelernt habe" entwickelt (www.20thingsilearned.com). Eine der Hauptideen hinter diesem Projekt war, dass es in Form eines Buches präsentiert werden sollte. Da es in dem Buch viel um offene Webtechnologien geht, fanden wir es wichtig, diesem Prinzip treu zu bleiben, indem bereits die äußere Form selbst veranschaulicht, was mit diesen Technologien heute möglich ist

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 waren der Ansicht, dass wir den Eindruck eines echten Buches am besten erwecken können, indem wir die positiven Komponenten der analogen Leseerfahrung simulierten und gleichzeitig in Bereichen wie der Navigation die digitalen Vorteile nutzten. Wir haben viel Arbeit in die grafische und interaktive Entwicklung des Leseflusses gesteckt, insbesondere in das Umblättern der Seiten.

Erste Schritte

In dieser Anleitung erfahren Sie, wie Sie mit dem Canvas-Element und einer Menge JavaScript Ihren eigenen Umblättern-Effekt erstellen. Ein Teil des grundlegenden Codes, wie Variablendeklarationen und Event-Listener-Subscription, wurden in den Snippets in diesem Artikel weggelassen. Denken Sie also daran, sich auch das voll funktionsfähige Beispiel anzusehen.

Sehen Sie sich am besten als Erstes die Demo an, damit Sie wissen, was unser Ziel ist.

Markup

Wir müssen immer daran denken, dass alles, was wir auf dem Canvas-Element erstellen, nicht von Suchmaschinen indexiert, von Nutzern ausgewählt oder über eine browserinterne Suche gefunden werden kann. Aus diesem Grund werden die Inhalte, mit denen wir arbeiten, direkt in die DOM-Struktur eingefügt und dann, falls verfügbar, mit JavaScript bearbeitet. Hierfür wird nur eine minimale Auszeichnung benötigt:

<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 sowie das canvas-Element enthält, auf dem wir die umblätternden Seiten erstellen. Innerhalb des section-Elements befindet sich ein div-Wrapper für den Inhalt. Diesen benötigen wir, damit wir die Breite der Seite bearbeiten können, ohne das Layout der Inhalte zu verändern. Der div-Wrapper verfügt über eine feste Breite und das section-Element ist so eingestellt, dass dessen Überlauf ausgeblendet wird. Dies führt dazu, dass die Breite des section-Elements als horizontale Maske für den div-Wrapper fungiert.

Offenes Buch
Ein Hintergrundbild mit der Papierstruktur und einer braunen Buchhülle werden zum "book"-Element hinzugefügt.

Logik

Der Code für den Umblättern-Effekt ist nicht sehr kompliziert, aber relativ umfangreich, da er viele prozedural generierte Grafiken umfasst. Sehen wir uns zunächst die Beschreibung der konstanten Werte an, die wir in dem 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;

Der Wert CANVAS_PADDING wird um das Canvas-Element herum hinzugefügt, damit das Papier beim Umblättern über das Buch hinaus ausgedehnt werden kann. Einige der hier definierten Konstanten sind auch in CSS festgelegt. Wenn Sie also die Größe des Buches ändern möchten, müssen Sie die Werte auch dort aktualisieren.

Konstanten
Die konstanten Werte, die in dem gesamten Code verwendet werden, um die Interaktion zu verfolgen und das Umblättern darzustellen

Als Nächstes müssen wir für jede Seite ein "Flip"-Objekt erstellen. Diese Objekte werden ständig aktualisiert, während 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
  });
}

Zunächst müssen wir darauf achten, dass die Seiten richtig übereinander angeordnet sind, indem wir die Z-Indizes der "section"-Elemente so organisieren, dass die erste Seite oben und die letzte unten ist. Die wichtigsten Eigenschaften der Flip-Objekte sind die Werte progress und target. Mit diesen wird festgelegt, wie weit die Seite aktuell umgeblättert werden soll. -1 bedeutet ganz nach links, 0 steht für die genaue Mitte des Buches und +1 bedeutet ganz nach rechts.

Fortschritt
Die "progress"- und "target"-Werte werden verwendet, um zu bestimmen, wo auf einer Skala von -1 bis +1 die umblätternde Seite dargestellt werden soll.

Nachdem wir ein Flip-Objekt für jede Seite definiert haben, müssen wir die Eingabe des Nutzers erfassen und verwenden, 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 && page - 1 >= 0) {
      // We are on the left side, drag the previous page
      flips[page - 1].dragging = true;
    }
    else if (mouse.x > 0 && 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;
  }
}

Mit der mouseMoveHandler-Funktion wird das mouse-Objekt aktualisiert, sodass wir uns stets an der letzten Cursorposition ausrichten.

Wir überprüfen in mouseDownHandler zunächst, ob auf der linken oder rechten Seite mit der Maus geklickt wurde, damit wir wissen, in welche Richtung wir mit dem Umblättern beginnen sollen. Wir vergewissern uns außerdem, dass in dieser Richtung eine weitere Seite folgt, da wir uns auch auf der ersten oder letzten Seite befinden könnten. Ist nach dieser Überprüfung eine gültige Option zum Umblättern verfügbar, wird die dragging-Markierung des entsprechenden Flip-Objekts auf true gesetzt.

Sobald wir den mouseUpHandler erreichen, überprüfen wir alle flips daraufhin, ob eines davon als dragging markiert ist und nun ausgelöst werden sollte. Wenn ein Flip-Objekt ausgelöst wird, wird sein "target"-Wert basierend auf der aktuellen Mausposition auf die Seite eingestellt, in die umgeblättert werden soll. Die Seitenzahl wird ebenfalls aktualisiert.

Rendering

Da nun der größte Teil unserer Logik fertig ist, sehen wir uns an, wie das umblätternde Papier auf dem Canvas-Element gerendert wird. Das meiste davon spielt sich innerhalb der render()-Funktion ab, die 60-mal pro Sekunde aufgerufen wird, um den aktuellen Status aller aktiven Flips zu aktualisieren und darzustellen.

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 die flips rendern, setzen wir das Canvas-Element mithilfe der Methode clearRect(x,y,w,h) zurück. Das Zurücksetzen des gesamten Canvas-Bereichs kostet viel Leistung und es wäre wesentlich effizienter, nur die Regionen zu löschen, die wir tatsächlich verwenden. Um aber nicht vom Thema abzukommen, setzen wir hier den gesamten Canvas-Bereich zurück.

Wenn ein Umblättern-Effekt ausgeführt wird, wird dessen target-Wert entsprechend der aktuellen Mausposition aktualisiert. Dazu verwenden wir jedoch eine Skala von -1 bis +1 statt der tatsächlichen Pixel. Wir erhöhen außerdem den progress-Wert um einen Bruchteil der Entfernung zum target. So erzeugen wir einen gleichmäßigen und lebendigen Verlauf des Umblätterns, da auf jedem Frame eine Aktualisierung stattfindet.

Da wir alle flips auf jedem Frame durchlaufen, müssen wir darauf achten, nur die aktiven Objekte zu aktualisieren. Ein Flip-Objekt wird als aktiv angesehen, wenn es sich nicht sehr nah am Buchrand befindet (innerhalb von 0,3 % der BOOK_WIDTH) oder wenn es als dragging markiert ist.

Da nun die gesamte Logik an ihrem Platz ist, müssen wir die grafische Darstellung eines Umblättern-Effekts auf der Basis seines aktuellen Status realisieren. Sehen wir uns dazu den ersten Teil der Funktion drawFlip(flip) 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";

Dieser Codeabschnitt beginnt mit der Berechnung einer Anzahl visueller Variablen, die wir brauchen, um das Umblättern realistisch darzustellen. Hier spielt der progress-Wert unseres Flip-Objekts eine wichtige Rolle, da dort die Falte erscheinen soll. Um dem Umblättern-Effekt Tiefe zu verleihen, dehnen wir die Seite über die oberen und unteren Buchkanten hinweg aus. Dieser Effekt ist am stärksten, wenn sich die Seite nah am Buchrücken befindet.

Umblättern
So sieht die Seite aus, wenn sie umgeblättert oder gezogen wird.

Nachdem nun alle Werte vorbereitet sind, müssen wir nur noch das Papier darstellen.

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 & 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 des Canvas API wird verwendet, um das Koordinatensystem so zu verschieben, dass wir für unseren Umblättern-Effekt den oberen Punkt des Buchrückens als 0,0-Position verwenden können. Beachten Sie, dass wir die aktuelle Transformationsmatrix des Canvas-Elements mit save() speichern und mit restore() wiederherstellen müssen, wenn wir fertig sind.

Translate-Methode
Von diesem Punkt aus erstellen wir den Umblättern-Effekt. Der ursprüngliche 0,0-Punkt befindet sich oben links im Bild, aber dadurch, dass wir dies mit "translate(x,y)" ändern, vereinfachen wir die Zeichenlogik.

Mithilfe von foldGradient füllen wir die Form des gefalteten Papiers aus, um ihm realistische Hervorhebungen und Schatten zu verleihen. Wir fügen außerdem eine sehr dünne Linie um die Seite herum hinzu, damit sie auch auf einem hellen Hintergrund gut zu sehen ist.

Jetzt brauchen wir nur noch die Form der umblätternden Seite mithilfe unserer oben definierten Eigenschaften darzustellen. Die linke und die rechte Kante unserer Seite werden als gerade Linien dargestellt. Die obere und die untere Kante sind geschwungen, um den Eindruck einer gekrümmten Seite beim Umblättern zu vermitteln. Die Stärke dieser Seitenkrümmung wird vom Wert verticalOutdent bestimmt.

Das ist alles! Sie verfügen jetzt über einen voll funktionsfähigen Umblättern-Effekt.

Demo zum Umblättern-Effekt

Beim Umblättern-Effekt geht es vor allem darum, das richtige interaktive Gefühl zu vermitteln. Bilder alleine werden dem nicht gerecht. Nutzen Sie die Links unten, um mit dem Ergebnis zu experimentieren!

Funktionsfähiges Beispiel ansehen

Quellcode herunterladen (75 K-Zip-Datei)

Nächste Schritte

Hard-Flip
Der weiche Umblättern-Effekt aus dieser Anleitung ist noch effizienter, wenn er mit anderen Buchelementen, zum Beispiel einem interaktiven festen Einband, kombiniert wird.

Dies ist nur ein Beispiel dafür, was mit HTML5-Funktionen wie dem Canvas-Element möglich ist. Ich möchte Ihnen auch die veredelte Bucherfahrung empfehlen, aus der diese Technik stammt: www.20thingsilearned.com. Dort sehen Sie, wie der Umblättern-Effekt in einer echten Anwendung zum Einsatz kommt und wie überzeugend er in Kombination mit anderen HTML5-Funktionen ist.

Referenzen

Comments

0