مقدمة
في عام 2010، تعاونت F-i.com مع فريق Google Chrome لإنشاء تطبيق ويب تعليمي يستند إلى HTML5 بعنوان "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-index لعناصر الأقسام، بحيث تكون الصفحة الأولى في الأعلى والأخيرة في الأسفل. أهم خصائص عناصر التقليب هي القيمتان progress وtarget.
تُستخدَم هذه القيم لتحديد مدى طي الصفحة حاليًا، حيث يشير الرقم -1 إلى الطي الكامل إلى اليسار، ويشير الرقم 0 إلى الطي في المنتصف تمامًا، ويشير الرقم 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 && 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;
}
}
تعدّل الدالة 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 & 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() إليها
عند الانتهاء من الرسم.
foldGradient هو ما سنملأ به شكل الورقة المطوية
لإضافة لمسات واقعية من الألوان البارزة والظلال. نضيف أيضًا خطًا رفيعًا جدًا حول الرسم الورقي حتى لا يختفي عند وضعه على خلفيات فاتحة.
كل ما تبقى الآن هو رسم شكل الورقة المطوية باستخدام الخصائص التي حدّدناها أعلاه. تم رسم الجانبَين الأيمن والأيسر من الورقة على شكل خطوط مستقيمة، بينما تم رسم الجانبَين العلوي والسفلي على شكل منحنيات لإضفاء إحساس بأنّ الورقة مطوية. يتم تحديد قوة انحناء الورقة هذا من خلال القيمة verticalOutdent.
هذا كل شيء! أصبح لديك الآن نظام تنقّل كامل الوظائف يتيح تصفّح الصفحات.
عرض توضيحي لتقليب الصفحات
لا يمكن وصف تأثير قلب الصفحة بشكل دقيق من خلال الصور، لأنّ هذا التأثير يهدف إلى إيصال الإحساس التفاعلي الصحيح.
الخطوات التالية
هذا مثال واحد فقط على ما يمكن تحقيقه من خلال استخدام ميزات HTML5، مثل عنصر اللوحة. أنصحك بإلقاء نظرة على تجربة الكتاب الأكثر دقة التي تم اقتباس هذه التقنية منها على الرابط: www.20thingsilearned.com. ستلاحظ هناك كيف يمكن تطبيق تقنية قلب الصفحات في تطبيق حقيقي ومدى فعاليتها عند استخدامها مع ميزات HTML5 الأخرى.
المراجع
- مواصفات واجهة برمجة التطبيقات Canvas