دراسة حالة - تأثير تقليب الصفحة من 20thingsilearned.com

مقدمة

في عام 2010، تعاون F-i.com وفريق Google Chrome في إنشاء تطبيق ويب تعليمي مستند إلى HTML5 يسمى 20 Things I Learned about Browses and the Web (www.20thingsilearned.com). كانت إحدى الأفكار الرئيسية وراء هذا المشروع أنه من الأفضل تقديمه في سياق الكتاب. ونظرًا لأن محتوى الكتاب يتعلق إلى حد كبير بتكنولوجيات الويب المفتوحة، رأينا أنه من المهم الحفاظ على الصدق من خلال جعل الحاوية نفسها مثالاً لما تتيح لنا هذه التقنيات تحقيقه اليوم.

غلاف الكتاب والصفحة الرئيسية لـ "20 شيئًا تعلمته عن المتصفحات والويب"
غلاف الكتاب والصفحة الرئيسية لـ "20 شيئًا تعلمته عن المتصفحات والويب" (www.20thingsilearned.com)

قررنا أن أفضل طريقة لتحقيق شعور أحد الكتب في العالم الحقيقي هو محاكاة الأجزاء الجيدة من تجربة القراءة التناظرية مع الاستمرار في الاستفادة من فوائد المجال الرقمي في مجالات مثل التنقل. تم بذل الكثير من الجهد في المعالجة الرسومية والتفاعلية لتدفق القراءة - خاصة كيفية انتقال صفحات الكتب من صفحة إلى أخرى.

البدء

يرشدك هذا البرنامج التعليمي خلال عملية إنشاء تأثير قلب الصفحة باستخدام عنصر لوحة الرسم والكثير من JavaScript. بعض التعليمات البرمجية البدائية، مثل التعريفات المتغيّرة واشتراك مستمع الأحداث، تم استبعادها من المقتطفات في هذه المقالة، لذا تذكر الرجوع إلى المثال العامل.

قبل البدء، من الأفضل مشاهدة العرض التوضيحي حتى تعرف ما نسعى إلى إنشائه.

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 برنامج تضمين div للمحتوى. نحتاج إلى هذا البرنامج لإتاحة إمكانية تغيير عرض الصفحة بدون التأثير في تنسيق محتواها. يتميز عنصر div بعرض ثابت وتم ضبط section على إخفاء تجاوزه، ويؤدي ذلك إلى استخدام عرض section كقناع أفقي لـ div.

افتح الكتاب.
تتم إضافة صورة خلفية تحتوي على زخرفة على شكل ورق وسُترة كتاب بنية إلى عنصر الكتاب.

منطق

إنّ الكود المطلوب لتشغيل قلب الصفحة ليس معقدًا للغاية، ولكنه واسع للغاية لأنه يتضمّن الكثير من الرسومات التي يتم إنشاؤها إجرائيًا. لنبدأ بإلقاء نظرة على وصف القيم الثابتة التي سنستخدمها خلال التعليمة البرمجية.

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 إلى مقياس +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) في لوحة الرسم لمعادلة تنسيق النظام لكي نتمكّن من رسم قلب الصفحة من أعلى العمود الفقري الذي يمثّل الموضع 0.0. تجدر الإشارة إلى أننا نحتاج أيضًا إلى save() مصفوفة التحويل الحالية للوحة الرسم وrestore() إليها عند الانتهاء من الرسم.

ترجمة
هذه هي النقطة التي نستند من خلالها إلى قلب الصفحة. تظهر النقطة 0.0 الأصلية في أعلى يسار الصورة، ولكن من خلال تغيير ذلك، يمكننا تبسيط منطق الرسم من خلال الترجمة(x,y).

foldGradient هو ما سنملأ به شكل الورقة المطوية لإعطائها ميزات وظلال واقعية. نضيف أيضًا خطًا رفيع جدًا حول الرسم الورقي حتى لا يختفي الورقة عند وضعه على خلفيات فاتحة.

كل ما تبقى الآن هو رسم شكل الورقة المطوية باستخدام الخصائص التي حددناها أعلاه. يتم رسم الجانبين الأيسر والأيمن من ورقتنا كخطوط مستقيمة والجانبان العلوي والسفلي منحنيان لإظهار الشعور بالانحناء بورقة قابلة للطي. يتم تحديد قوة هذا الانحناء الورقي من خلال القيمة verticalOutdent.

أكملت هذه الخطوة. لقد حصلت الآن على ميزة التنقل بقلب الصفحة وتكون تعمل بكامل طاقتها.

عرض توضيحي لقلب الصفحة

يدور تأثير قلب الصفحة حول توصيل الإحساس التفاعلي الصحيح، لذا فإن النظر إلى صوره لا يفعل ذلك بالضبط.

الخطوات التالية

بغطاء صلب
يصبح قلب الصفحة بشكل بسيط في هذا البرنامج التعليمي أكثر فعالية عند إقرانه بميزات أخرى شبيهة بالكتاب، مثل الغلاف الكرتوني التفاعلي.

هذا مثال واحد فقط على ما يمكن تحقيقه باستخدام ميزات HTML5 مثل عنصر لوحة الرسم. أنصحك بإلقاء نظرة على تجربة الكتب الأكثر تحسينًا التي يمكنك الاطّلاع عليها من خلال هذه التقنية: www.20thingsilearned.com. وهناك، سترى كيف يمكن تطبيق تقلبات الصفحات في تطبيق فعلي ومدى فعاليتها عند إقرانها بميزات HTML5 أخرى.

المراجع

  • مواصفات واجهة برمجة التطبيقات Canvas