مقدمه
در سال 2010، Fi.com و تیم Google Chrome بر روی یک برنامه وب آموزشی مبتنی بر HTML5 به نام 20 چیزهایی که درباره مرورگرها و وب یاد گرفتم ( www.20thingsilearned.com ) همکاری کردند. یکی از ایده های کلیدی پشت این پروژه این بود که بهتر است در چارچوب یک کتاب ارائه شود. از آنجایی که محتویات کتاب تا حد زیادی در مورد فناوریهای وب باز است، ما احساس کردیم که مهم است که با ساختن ظرف خود نمونهای از آنچه این فناوریها امروز به ما اجازه میدهند به آن وفادار بمانیم.
ما تصمیم گرفتیم که بهترین راه برای دستیابی به احساس یک کتاب دنیای واقعی، شبیهسازی بخشهای خوب تجربه خواندن آنالوگ است و در عین حال از مزایای قلمرو دیجیتال در زمینههایی مانند ناوبری استفاده میکنیم. تلاش زیادی برای پردازش گرافیکی و تعاملی جریان خواندن انجام شد - به ویژه اینکه چگونه صفحات کتاب ها از صفحه ای به صفحه دیگر می چرخند.
شروع به کار
این آموزش شما را با استفاده از عنصر بوم و مقدار زیادی از جاوا اسکریپت طی فرآیند ایجاد افکت برگردان صفحه خود راهنمایی می کند. برخی از کدهای ابتدایی، مانند اعلانهای متغیر و اشتراک شنونده رویداد، در بخشهای این مقاله حذف شدهاند، بنابراین به یاد داشته باشید که به مثال کاری اشاره کنید.
قبل از شروع، ایده خوبی است که نسخه آزمایشی را بررسی کنید تا بدانید هدف ما از ساخت چه چیزی است.
نشانه گذاری
همیشه مهم است که به یاد داشته باشید آنچه را که ما روی بوم می کشیم نمی تواند توسط موتورهای جستجو نمایه شود، توسط یک بازدید کننده انتخاب شود یا توسط جستجوهای درون مرورگر پیدا شود. به همین دلیل، محتوایی که ما با آن کار خواهیم کرد مستقیماً در DOM قرار می گیرد و در صورت موجود بودن توسط جاوا اسکریپت دستکاری می شود. نشانه گذاری مورد نیاز برای این حداقل است:
<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 به معنای سمت راست ترین لبه کتاب است.
اکنون که یک شی flip برای هر صفحه تعریف شده است، باید شروع به گرفتن و استفاده از ورودی کاربران برای به روز رسانی وضعیت تلنگر کنیم.
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
ما با بررسی اینکه آیا ماوس در سمت چپ یا راست فشار داده شده است، شروع می کنیم تا بدانیم که می خواهیم به سمت کدام سمت حرکت کنیم. ما همچنین اطمینان حاصل می کنیم که صفحه دیگری در آن جهت وجود دارد زیرا ممکن است در صفحه اول یا آخرین صفحه باشیم. اگر بعد از این بررسیها یک گزینه flip معتبر موجود باشد، پرچم dragging
شی flip مربوطه را روی true
تنظیم میکنیم.
هنگامی که به mouseUpHandler
رسیدیم، تمام flips
را مرور می کنیم و بررسی می کنیم که آیا هر یک از آنها به عنوان dragging
علامت گذاری شده اند و اکنون باید آزاد شوند. هنگامی که یک تلنگر آزاد می شود، مقدار هدف آن را مطابق با سمتی که باید به آن برگرداند بسته به موقعیت فعلی ماوس تنظیم می کنیم. شماره صفحه نیز بهروزرسانی میشود تا این پیمایش را منعکس کند.
رندرینگ
اکنون که بیشتر منطق ما سر جای خود است، نحوه رندر کردن کاغذ تاشو بر روی عنصر بوم را بررسی خواهیم کرد. بیشتر اینها در داخل تابع render()
اتفاق می افتد که 60 بار در ثانیه برای به روز رسانی و ترسیم وضعیت فعلی تمام flipsهای فعال فراخوانی می شود.
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
آن را بهروزرسانی میکنیم تا با موقعیت ماوس مطابقت داشته باشد اما در مقیاس ۱- تا ۱ به جای پیکسلهای واقعی. ما همچنین 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 API