מקרה לדוגמה – אפקט היפוך דפים מ-20thingsilearned.com

Hakim El Hattab
Hakim El Hattab

מבוא

ב-2010, F-i.com והצוות של Google Chrome שיתפו פעולה בבניית אפליקציית אינטרנט חינוכית מבוססת HTML5 בשם '20 דברים שלמדתי על דפדפנים והאינטרנט' (www.20thingsilearned.com). אחד מהרעיונות המרכזיים של הפרויקט הזה היה הצגתו בהקשר של ספר. מכיוון שהתוכן של הספר עוסק בעיקר בטכנולוגיות אינטרנט פתוחות, חשוב לנו להמשיך להיות נאמן לזה בכך שהקונטיינר עצמו הוא דוגמה למה שהטכנולוגיות האלה מאפשרות לנו להשיג היום.

כריכת הספר ודף הבית של הספר '20 דברים שלמדתי על דפדפנים והאינטרנט'
הכריכה ודף הבית של הספר "20 דברים שלמדתי על דפדפנים והאינטרנט" (www.20thingsilearned.com)

החלטנו שהדרך הטובה ביותר ליצור את התחושה של ספר בעולם האמיתי היא לדמות את החלקים הטובים בחוויית הקריאה האנלוגית, תוך למנף את היתרונות של התחום הדיגיטלי בתחומים כמו ניווט. מאמצים רבים הושקעו בטיפול הגרפי והאינטראקטיבי של זרימת הקריאה - במיוחד באופן שבו דפי הספרים עוברים מדף לדף.

תחילת העבודה

במדריך הזה תלמדו על תהליך היצירה של אפקט היפוך דפים באמצעות רכיב בד הקנבס ושפע של JavaScript. חלק מהקוד הבסיסי, כמו הצהרות משתנים ומינוי ל-event listener, לא מוצגים בקטעי הקוד במאמר הזה, כך שכדאי להתייחס לדוגמה שעובדת.

לפני שנתחיל, כדאי לבדוק את ההדגמה כדי להבין מה אנחנו מתכוונים ליצור.

Markup

תמיד חשוב לזכור שמנועי חיפוש לא יכולים להוסיף לאינדקס את התוכן שאנחנו משרטטים על בד קנבס, שנבחרו על ידי מבקר או חיפושים בדפדפן. לכן, התוכן שנעבוד איתו מועבר ישירות ל-DOM ואז משתנה על ידי JavaScript אם הוא זמין. תגי העיצוב הנדרשים לכך הם מינימליים:

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

יש לנו רכיב קונטיינר ראשי אחד עבור הספר, שמכיל את הדפים השונים של הספר ואת הרכיב canvas שעליו נצייר את הדפים ההופכים. בתוך האלמנט section יש wrapper של div עבור התוכן. אנחנו צריכים שהיא תוכל לשנות את רוחב הדף בלי להשפיע על פריסת התוכן. ל-div יש רוחב קבוע וה-section מוגדר להסתיר את הגלישה שלו, וכתוצאה מכך יתקבל רוחב ה-section כמסכה אופקית עבור div.

פותחים את &#39;ספר&#39;.
לרכיב הספר נוספה תמונת רקע שמכילה את טקסטורת הנייר ואת ז'קט הספר החום.

לוגיקה

הקוד שנדרש כדי להפעיל את המעבר בין הדפים הוא לא מאוד מורכב, אבל הוא מקיף למדי, כי הוא כולל רכיבים גרפיים רבים שנוצרים באופן פרוצדורלי. נתחיל בבחינת התיאור של הערכים הקבועים שנשתמש בהם לאורך הקוד.

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;

מוסיפים את CANVAS_PADDING מסביב לקנבס, כדי שנוכל להפוך את הנייר מחוץ לספר. שימו לב שחלק מהקבועים המוגדרים כאן מוגדרים גם ב-CSS, כך שאם אתם רוצים לשנות את גודל הספר, תצטרכו לעדכן גם את הערכים שם.

קבועים.
הערכים הקבועים שבהם נעשה שימוש בכל הקוד כדי לעקוב אחר אינטראקציות ולהעביר דפים.

בשלב הבא עלינו להגדיר אובייקט היפוך עבור כל דף, הם יעודכנו באופן קבוע תוך כדי אינטראקציה עם הספר כדי לשקף את הסטטוס הנוכחי של ההיפוך.

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

קודם כל, אנחנו צריכים לוודא שהדפים נמצאים בשכבות נכונות על ידי ארגון של אינדקסי ה-z של רכיבי הקטע כך שהדף הראשון יהיה בחלק העליון והדף האחרון נמצא בתחתית. המאפיינים החשובים ביותר של האובייקטים ההופכים הם הערכים progress ו-target. הערכים האלה משמשים כדי לקבוע את המרחק הנוכחי שבו הדף יכול לקפל, -1 פירושו המרחק השמאלי מהספר, 0 פירושו מרכז "המת" של הספר ו-1+ מציין את הקצה הימני ביותר של הספר.

התקדמות.
ההתקדמות וערכי היעד של הכפלות משמשים כדי לקבוע את המיקום שבו יש לצייר את הדף המתקפל בסולם של -1 עד 1+.

עכשיו, אחרי שמגדירים אובייקט היפוך לכל דף, אנחנו צריכים להתחיל לתעד ולהשתמש בקלט של המשתמשים כדי לעדכן את מצב ההיפוך.

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

הפונקציה mouseMoveHandler מעדכנת את האובייקט mouse כך שתמיד נפעל לקראת מיקום הסמן העדכני ביותר.

ב-mouseDownHandler, אנחנו מתחילים בבדיקה אם העכבר נלחץ בדף השמאלי או בדף הימני, כדי שנדע באיזה כיוון אנחנו רוצים להתחיל לנוע. אנחנו גם מוודאים שיש דף נוסף בכיוון הזה, כי אנחנו עשויים להיות בדף הראשון או האחרון. אם יש אפשרות היפוך חוקית אחרי הבדיקות האלה, אנחנו מגדירים את הדגל dragging של אובייקט ההיפוך התואם ל-true.

אחרי שנגיע אל mouseUpHandler, נעבור על כל flips ונבדוק אם אחד מהם סומן כ-dragging ועכשיו צריך לשחרר אותו. כשמשחררים היפוך, אנחנו מגדירים את ערך היעד כך שיתאים לצד שעליו רוצים להפוך בהתאם למיקום הנוכחי של העכבר. גם מספר הדף מתעדכן כדי לשקף את הניווט הזה.

רינדור

עכשיו, לאחר שרוב ההיגיון שלנו יושם, נסביר איך לעבד את הנייר המתקפל לרכיב הקנבס. רוב התהליך הזה מתרחש בתוך הפונקציה render(), שנקראת 60 פעמים בשנייה, כדי לעדכן ולצייר את המצב הנוכחי של כל ההיפוך הפעילות.

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

}
}

לפני שנתחיל לעבד את flips, איפסנו את לוח הציור באמצעות השיטה clearRect(x,y,w,h). ניקוי של כל הקנבס גורם לפגיעה משמעותית בביצועים, ועדיף לפנות רק את האזורים שעליהם אנחנו מסתמכים. כדי לשמור על נושא המדריך הזה, נשאיר את השטח לנקות את כל לוח הציור.

במקרה שגוררים היפוך, אנחנו מעדכנים את הערך של השדה target כך שיתאים למיקום העכבר, אבל בקנה מידה של 1-1, ולא פיקסלים בפועל. אנחנו גם מגדילים את progress בשבריר מהמרחק ל-target, והתוצאה תהיה התקדמות חלקה ומונפשת של ההיפוך כי הוא מתעדכן בכל פריים.

מכיוון שאנחנו עוברים על כל flips בכל פריים, אנחנו צריכים לוודא שאנחנו משרטטים מחדש רק את אלה שפעילים. אם היפוך לא קרוב מאוד לקצה הספר (ב-0.3% מ-BOOK_WIDTH), או אם הוא מסומן כ-dragging, הוא נחשב פעיל.

עכשיו, אחרי שמיישמים את כל הלוגיקה, אנחנו צריכים לשרטט את הייצוג הגרפי של היפוך בהתאם למצב הנוכחי שלה. זה הזמן לבחון את החלק הראשון של הפונקציה 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';

החלק הזה של הקוד מתחיל בחישוב של מספר משתנים ויזואליים שעלינו להשתמש בחלק מהם באופן מציאותי. לערך של progress של ההיפוך שאנחנו משרטטים יש כאן תפקיד חשוב, כי זה המקום שבו אנחנו רוצים להציג את קפל הדפים. כדי להוסיף עומק לאפקט של היפוך הדפים, אנחנו מוסיפים את הנייר אל מחוץ לקצה העליון והתחתון של הספר. האפקט מופיע בשיאו כשההיפוך קרוב לעמוד השדרה של הספר.

היפוך
כך נראה קיפול של דף כשהדף הופך או גוררים אותו.

עכשיו, לאחר שכל הערכים מוכנים, כל מה שנשאר זה לצייר את הנייר!

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

השיטה translate(x,y) ב-canvas API משמשת לקיזוז מערכת הקואורדינטות, כך שנוכל לשרטט את היפוך הדפים עם החלק העליון של עמוד השדרה שפועל כמיקום 0,0. שימו לב שאנחנו צריכים גם save() את מטריצת הטרנספורמציה הנוכחית של הקנבס ואת restore() אליה בסיום השרטוט.

תרגום
זו הנקודה שממנה אנחנו מחשבים את מעבר הדפים. נקודת ה-0,0 המקורית נמצאת בפינה השמאלית העליונה של התמונה, אבל אם משנים אותה, באמצעות Translate(x,y), אנחנו מפשטים את לוגיקת השרטוט.

באמצעות השדה foldGradient נמלא את הצורה של הנייר המקופל כדי להציג אזורים בהירים וצלליות. אנחנו גם מוסיפים קו דק מאוד מסביב לשרטוט הנייר, כדי שהנייר לא ייעלם כשהוא על רקע בהיר.

עכשיו נותר רק לצייר את הצורה של הנייר המקופל לפי המאפיינים שהגדרנו למעלה. הצד השמאלי והימני של הנייר משורטט בקווים ישרים, והחלק העליון והתחתון מעוקלים כדי להעניק תחושה של קיפול של נייר. העוצמה של כיפוף הנייר הזה נקבעת על סמך הערך verticalOutdent.

זהו! עכשיו יש לכם פונקציונליות מלאה לניווט בדפים.

הדגמה של מעבר בין דפים

אפקט היפוך הדפים נועד להציג את התחושה האינטראקטיבית הנכונה, כך שצפייה בתמונות לא רק יוצרת הצדקה.

השלבים הבאים

היפוך קשיח
היפוך הדפים הרך במדריך הזה הופך לעוצמתי עוד יותר כשמשלבים אותם עם תכונות אחרות דמויי ספר, כמו כריכה קשה אינטראקטיבית.

זו רק דוגמה אחת למה שניתן להשיג באמצעות תכונות של HTML5 כמו רכיב הקנבס. אני ממליץ לך לצפות בחוויית הספר המשופרת, שבה השיטה הזו היא קטע בכתובת: www.20thingsilearned.com. שם תראו כיצד ניתן להפוך את הדפים באפליקציה אמיתית ומהו העוצמה שתתקבל כשמשלבים אותם עם תכונות HTML5 אחרות.

קובצי עזר