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.
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.
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.
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.
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 && 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;
}
}
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.
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 & 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.
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
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
- Canvas API-Spezifikation