مقدمة
في عام 2010، تعاون فريق F-i.com مع فريق Google Chrome في إنشاء تطبيق ويب تعليمي مستند إلى HTML5 باسم 20 Things I Learned about Browsers and the Web (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 أنّ الصفحة مطوية بالكامل إلى أقصى يسار الكتاب.
الآن بعد أن حدّدنا عنصر انعكاس لكل صفحة، علينا بدء تسجيل إدخالات المستخدمين واستخدامها لتعديل حالة الانعكاس.
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
، نعيد ضبط canvas باستخدام طريقة 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)
في واجهة برمجة تطبيقات Canvas API لتحييد
نظام الإحداثيات حتى نتمكّن من رسم عملية قلب الصفحة مع وضع أعلى
العمود الفقري في موضع 0,0. يُرجى العلم أنّنا نحتاج أيضًا إلى save()
مصفوفة التحويل الحالية للوحة وrestore()
إليها
عند الانتهاء من الرسم.
foldGradient
هو الشكل الذي سنملؤه بالورق المطوي
لإضفاء تأثيرات الإضاءة والتظليل الواقعية عليه. نضيف أيضًا خطًا رفيعًا جدًا
حول الرسم على الورق حتى لا يختفي الورقة عند وضعها
على خلفيات فاتحة.
كل ما يتبقى الآن هو رسم شكل الورقة المطوية باستخدام
السمات التي حدّدناها أعلاه. يتم رسم الجانبَين الأيمن والأيسر من الورقة
كخطوط مستقيمة، ويتم رسم الجانبَين العلوي والسفلي بشكل منحني لإضفاء مظهر الانحناء
على الورقة. يتم تحديد قوة انحناء هذا الورق
حسب قيمة verticalOutdent
.
هذا كل شيء! أصبح لديك الآن تنقّل وظيفي بالكامل لقلب الصفحات.
عرض توضيحي لميزة "قلب الصفحة"
يهدف تأثير قلب الصفحة إلى توفير تجربة تفاعلية مناسبة، لذا لا يقدّم عرض الصور هذا التأثير حقّه.
الخطوات التالية
هذا مثال واحد فقط على ما يمكن تحقيقه من خلال استخدام ميزات HTML5 ، مثل عنصر اللوحة. أنصحك بإلقاء نظرة على تجربة الكتاب المُحسَّنة التي تم اقتباس هذه التقنية منها على الرابط: www.20thingsilearned.com. ستعرِض عليك هذه التجربة كيفية تطبيق ميزة قلب الصفحة في تطبيق حقيقي ومدى فعاليتها عند استخدامها مع ميزات HTML5 الأخرى.
المراجع
- مواصفات واجهة برمجة التطبيقات Canvas