رسم گرافیک با جاوا اسکریپت (بخش دوم) — راهنمای جامع
در بخش قبلی این مطلب با عنوان رسم گرافیک با جاوا اسکریپت در مورد برخی مباحث مقدماتی مرتبط با رسم گرافیکهای دو بعدی در جاوا اسکریپت عنصر Canvas صحبت کردیم. اینک بخش دوم آن را ارائه میکنیم. برای مطالعه بخش قبلی روی لینک زیر کلیک کنید:
حلقهها و انیمیشنها
تا به اینجا برخی از کاربردهای اولیه بوم 2 بعدی را معرفی کردیم، اما در عمل تا زمانی که بوم را بهروزرسانی یا انیمیت نکردهاید هنوز از قدرت کامل بوم استفاده نکردهاید. در نهایت بوم، تصاویر قابل اسکریپتنویسی عرضه میکند. اگر قصد ندارید چیزی را تغییر دهید، در این صورت شاید بهتر باشد از تصاویر استاتیک استفاده و در وقت خود نیز صرفهجویی کنید.
ایجاد یک حلقه
کار کردن با حلقهها در بوم تا حدی سرگرمکننده است، چون میتوانید دستورهای بوم را درست مانند همه کدهای جاوا اسکریپت درون یک حلقه for اجرا کنید. در ادامه یک مثال ارائه میکنیم.
یک کپی از فایل زیر روی سیستم خود تهیه کنید.
فایل canvas_template.html
1<!DOCTYPE html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <title>Canvas</title>
6 <style>
7 body {
8 margin: 0;
9 overflow: hidden;
10 }
11 </style>
12 </head>
13 <body>
14 <canvas class="myCanvas">
15 <p>Add suitable fallback here.</p>
16 </canvas>
17
18 <script>
19 var canvas = document.querySelector('.myCanvas');
20 var width = canvas.width = window.innerWidth;
21 var height = canvas.height = window.innerHeight;
22 var ctx = canvas.getContext('2d');
23 ctx.fillStyle = 'rgb(0,0,0)';
24 ctx.fillRect(0,0,width,height);
25 </script>
26 </body>
27</html>
فایل را در ادیتور کد باز کنید.
خط زیر را به انتهای جاوا اسکریپت اضافه کنید. این کد شامل یک متد جدید به نام ()translate است که نقطه مبدأ بوم را جابجا میکند:
ctx.translate(width/2, height/2);4
این امر موجب میشود که مبدأ (0, 0) به مرکز بوم جابجا شود و دیگر در گوشه چپ-بالا نباشد. این وضعیت در موقعیتهای زیادی مانند این مثال مفید است چون میخواهیم طراحی ما نسبت به مرکز بوم ترسیم شود.
سپس کد زیر را به انتهای جاوا اسکریپت اضافه کنید:
1function degToRad(degrees) {
2 return degrees * Math.PI / 180;
3};
4
5function rand(min, max) {
6 return Math.floor(Math.random() * (max-min+1)) + (min);
7}
8
9var length = 250;
10var moveOffset = 20;
11
12for(var i = 0; i < length; i++) {
13
14}
در کد فوق ما یک تابع ()degToRad پیادهسازی کردهایم که در مثال مثلث قبلی دیدیم. یک تابع ()a نیز وجود دارد که عددی تصادفی بین کرانهای بالا و پایین تعیینشده بازگشت میدهد، متغیرهای length و moveOffset و یک حلقه for خالی نیز وجود دارند.
ایده این است که چیزی روی بوم و درون حلقه for رسم کنیم و هر بار روی آن تکرار کنیم به طوری که بتوانیم چیز جالبی رسم کنیم. کد زیر را درون حلقه for اضافه کنید:
1ctx.fillStyle = 'rgba(' + (255-length) + ', 0, ' + (255-length) + ', 0.9)';
2ctx.beginPath();
3ctx.moveTo(moveOffset, moveOffset);
4ctx.lineTo(moveOffset+length, moveOffset);
5var triHeight = length/2 * Math.tan(degToRad(60));
6ctx.lineTo(moveOffset+(length/2), moveOffset+triHeight);
7ctx.lineTo(moveOffset, moveOffset);
8ctx.fill();
9
10length--;
11moveOffset += 0.7;
12ctx.rotate(degToRad(5));
بدین ترتیب در هر تکرار حلقه کارهای زیر صورت میگیرند:
- fillStyle به صورت سایهای از رنگ بنفش کمی شفاف تعیین میشود که هر بار بر مبنای مقدار length عوض میشود. چنان که در ادامه خواهیم دید، این length هر بار که حلقه اجرا میشود کوچکتر میشود و بدین ترتیب جلوهای که ایجاد میشود چنین است که رنگ هر بار با ترسیم مثلث بعدی روشنتر میشود.
- مسیر آغاز میشود.
- قلم به مختصات (moveOffset, moveOffset) جابجا میشود. این متغیر میزان فاصلهای که میخواهیم هر بار یک مثلث ترسیم شود تعیین میکند.
- یک خط به مختصات (moveOffset+length, moveOffset) رسم میکنیم. بدین ترتیب خطی به طول length موازی با محور X رسم میشود.
- ارتفاع مثلث را مانند قبل تعیین میکنیم.
- یک خط به گوشه رو به پایین مثلث رسم میکنیم و سپس یک خط به پشت نقطه آغاز مثلث میکشیم.
- ()fill برای پر کردن داخل مثلث فراخوانی میشود.
- متغیرهایی که توالی مثلثها را تعیین میکنند بهروزرسانی میکنیم، بنابراین میتوانیم برای رسم مثلث بعدی آماده باشیم. مقدار length را یک واحد افزایش میدهیم و از این رو مثلثها هر بار کوچکتر میشوند. moveOffset را مقدار کمی افزایش میدهیم، بنابراین هر بار که مثلث کمی دورتر میرود، از تابع ()rotate جدیدی استفاده میکنیم به طوری که میتوانیم کل بوم را بچرخانیم. ما آن را 5 درجه پیش از رسم مثلث بعدی میچرخانیم.
در نهایت مثال ما باید به شکل زیر درآمده باشد:
در این مرحله شما را تشویق میکنیم که با این مثال بیشتر کار کنید و تغییرات دلخواه خود را در آن ایجاد کنید. برای نمونه میتوانید:
- مستطیلها یا کمانهایی به جای مثلث رسم کنید یا حتی تصاویری جاسازی نمایید.
- با مقادیر length و moveOffset بازی کنید.
- اعدادی تصادفی با استفاده از تابع ()rand که قبلاً معرفی کردیم، ولی مورد استفاده قرار ندادیم، تولید کنید.
نکته: کد کامل این مثال به صورت زیر است:
1<!DOCTYPE html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <title>Canvas</title>
6 <style>
7 body {
8 margin: 0;
9 overflow: hidden;
10 }
11 </style>
12 </head>
13 <body>
14 <canvas class="myCanvas">
15 <p>Add suitable fallback here.</p>
16 </canvas>
17
18 <script>
19 var canvas = document.querySelector('.myCanvas');
20 var width = canvas.width = window.innerWidth;
21 var height = canvas.height = window.innerHeight;
22 var ctx = canvas.getContext('2d');
23 ctx.fillStyle = 'rgb(0,0,0)';
24 ctx.fillRect(0,0,width,height);
25 ctx.translate(width/2, height/2);
26 function degToRad(degrees) {
27 return degrees * Math.PI / 180;
28 };
29 function rand(min, max) {
30 return Math.floor(Math.random() * (max-min+1)) + (min);
31 }
32 var length = 250;
33 var moveOffset = 20;
34 for(var i = 0; i < length; i++) {
35 ctx.fillStyle = 'rgba(' + (255-length) + ',0,' + (255-length) + ',0.9)';
36 ctx.beginPath();
37 ctx.moveTo(moveOffset,moveOffset);
38 ctx.lineTo(moveOffset+length,moveOffset);
39 var triHeight = length/2 * Math.tan(degToRad(60));
40 ctx.lineTo(moveOffset+(length/2),moveOffset+triHeight);
41 ctx.lineTo(moveOffset,moveOffset);
42 ctx.fill();
43 length--;
44 moveOffset+=0.7;
45 ctx.rotate(degToRad(5));
46 }
47 </script>
48 </body>
49</html>
انیمیشنها
مثال حلقهای که در بخش قبل ساختیم جالب بود، اما شما به یک حلقه ثابت نیاز دارید که در همه اپلیکیشنهای جدی بوم مانند بازی یا بصریسازی آنی، به طور مکرر از آن استفاده کنید. اگر بوم را مانند یک فیلم تصور کنیم ما به یک نمایشگر نیاز داریم که در هر فریم بهروزرسانی شود تا نمای بهروز را نشان دهد. نرخ رفرش مناسب 60 فریم بر ثانیه است تا حرکتها به چشم انسان زیبا و روان باشند.
چند تابع جاوا اسکریپت وجود دارند که امکان اجرای مکرر تابعها را چندین بار در طی یک ثانیه فراهم میسازند، اما بهترین آنها برای منظور ما ()window.requestAnimationFrame است. این تابع یک پارامتر میگیرد که نام تابعی است که میخواهید در هر فریم اجرا شود. دفعه بعد که مرورگر آماده بهروزرسانی صفحه باشد، تابع شما فراخوانی میشود. اگر آن تابع بهروزرسانی جدیدی در انیمیشن شما رسم کند، در این صورت بار دیگر درست قبل از انتهای تابع فراخوانی میشود و حلقه انیمیشن همین طور تداوم مییابد. حلقه زمانی متوقف میشود که ()requestAnimationFrame را فراخوانی کنید و یا ()window.cancelAnimationFrame را پس از فراخوانی ()requestAnimationFrame اما قبل از فراخوانی فریم، اجرا کنید.
نکته: فراخوانی ()cancelAnimationFrame از کد اصلی در زمان پایان یافتن کارمان با انیمیشن، ایده خوبی است زیرا مطمئن میشویم که هیچ بهروزرسانی در انتظار اجرا شدن نیست.
مرورگر جزییات پیچیدهای مانند اجرای انیمیشن با سرعت یکنواخت و عدم هدر دادن منابع برای انیمیت چیزهایی که دیده نمیشوند را خود بر عهده میگیرد.
برای این که ببینید طرز کار آن چگونه است، مجدداً نگاهی سریع به مثال توپهای رقصان خود میاندازیم. کد حلقه که همه چیز را در حال حرکت حفظ میکند به صورت زیر است:
1function loop() {
2 ctx.fillStyle = 'rgba(0, 0, 0, 0.25)';
3 ctx.fillRect(0, 0, width, height);
4
5 for(let i = 0; i < balls.length; i++) {
6 balls[i].draw();
7 balls[i].update();
8 balls[i].collisionDetect();
9 }
10
11 requestAnimationFrame(loop);
12}
13
14loop();
ما تابع ()loop را یک بار در انتهای کد برای آغاز چرخه اجرا میکنیم و نخستین فریم انیمیشن را رسم میکنیم. سپس تابع ()loop مسئولیت فراخوانی (requestAnimationFrame(loop را برای اجرای فریم بعدی انیمیشن به طور مکرر بر عهده میگیرد.
توجه کنید که هر فریم به صورت کامل بوم را پاک و همه چیز را از تو رسم میکند. در مورد هر توپی که وجود دارد ما آن را رسم میکنیم، موقعیتش را بهروزرسانی میکنیم و بررسی میکنیم که آیا با توپهای دیگر برخورد دارد یا نه. زمانی که یک گرافیک روی بوم رسم میکنید، هیچ راهی برای دستکاری منفرد گرافیک مانند کاری که روی عناصر DOM انجام میدهیم وجود ندارد. ما نمیتوانیم هیچ توپی را روی بوم جابجا کنیم، زیرا زمانی که رسم شد جزئی از بوم میشود و عنصر یا شیء منفردی نیست که بتوان با آن تعامل داشت. به جای آن میتوان بوم را پاک کرد و دوباره نو رسم کرد و این کار یا از طریق پاک کردن و رسم مجدد همه چیز و یا با داشتن کدی که دقیقاً میداند کدام بخشها باید پاک شوند و تنها آن نواحی مورد نیاز مجدداً روی بوم رسم شوند ممکن است.
بهینهسازی انیمیشن گرافیکها یک بحث کاملاً تخصصی برنامهنویسی است که تکنیکهای هوشمندانه زیادی دارد. با این حال بررسی این موارد خارج از حیطه این مقاله است.
به طور کلی فرایند انیمیشن بوم شامل گامهای زیر است:
- محتوای بوم پاک میشود (به وسیله ()fillRect یا ()clearRect)
- حالت (در صورت نیاز) با استفاده از ()save ذخیره میشود. این مورد زمانی ضروری است که بخواهید تنظیماتی که روی بوم بهروزرسانی کردهاید، پیش از ادامه ذخیره شوند و برای کاربردهای پیشرفتهتر مفید است.
- گرافیکی که میخواهید انیمیت کنید را رسم کنید.
- تنظیماتی را که در گام 2 ذخیره کرده بودید با استفاده از ()restore بازیابی کنید.
- ()requestAnimationFrame را فراخوانی کنید تا رسم فریم بعدی انیمیشن زمانبندی شود.
نکته: ما در مثال خود به بررسی ()save و ()restore نپرداختیم، اما در مقالات بعدی این سری مطالب آموزشی به آن خواهیم پرداخت.
یک انیمیشن ساده کاراکتر
اینک نوبت به ایجاد یک انیمیشن ساده رسیده است. به این منظور یک کاراکتر از یک بازی جالب را انتخاب کردهایم که روی صفحه راه میرود. یک کپی از فایل زیر روی رایانه خود ایجاد کنید:
فایل canvas_template.html
1<!DOCTYPE html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <title>Canvas</title>
6 <style>
7 body {
8 margin: 0;
9 overflow: hidden;
10 }
11 </style>
12 </head>
13 <body>
14 <canvas class="myCanvas">
15 <p>Add suitable fallback here.</p>
16 </canvas>
17
18 <script>
19 var canvas = document.querySelector('.myCanvas');
20 var width = canvas.width = window.innerWidth;
21 var height = canvas.height = window.innerHeight;
22 var ctx = canvas.getContext('2d');
23 ctx.fillStyle = 'rgb(0,0,0)';
24 ctx.fillRect(0,0,width,height);
25 </script>
26 </body>
27</html>
آن را در ادیتور کد باز کنید. یک کپی از فایل walk-right.png (+) نیز در همان دایرکتوری ایجاد کنید. در انتهای جاوا اسکریپت خط زیر را اضافه کنید تا یک بار دیگر مختصات مقدار را در میانه بوم قرار دهیم:
1ctx.translate(width/2, height/2);
اکنون یک شیء HTMLImageElement جدید ایجاد کنید، src آن را تصویری که میخواهید بارگذاری شود تعیین کنید و یک دستگیره رویداد onload اضافه کنید که موجب میشود تابع ()draw زمانی که تصویر بارگذاری میشود اجرا گردد.
1var image = new Image();
2image.src = 'walk-right.png';
3image.onload = draw;
اکنون برخی متغیرها اضافه میکنیم تا رد موقعیت تصویری که روی صفحه رسم میشود و تعداد تصاویری که میخواهیم نمایش یابد را داشته باشیم.
1var sprite = 0;
2var posX = 0;
در ادامه تصویر spritesheet را توضیح میدهیم. تصویر به صورت زیر است:
این تصویر شامل شش اسپریت است که کل توالی راه رفتن را میسازند و هر یک 102 پیکسل عرض و 148 پیکسل ارتفاع دارند. برای نمایش هر اسپریت به صورت واضح باید از ()drawImage استفاده کنید تا یک تصویر را از اسپریتشیت برش داده و تنها آن بخش را نمایش دهد. این شبیه کاری است که در مورد لوگوی فایرفاکس قبلاً انجام دادیم. مختصات X قطعه باید مضربی از 102 باشد و مختصات Y همواره 0 است. اندازه قطعه همواره 102 در 148 پیکسل است.
اینک یک تابع ()draw خالی در انتهای کد درج میکنیم که آماده کدنویسی است:
1function draw() {
2};
بقیه کد در این بخش درون draw() قرار میگیرد. ابتدا خط زیر را اضافه میکنیم که بوم را پاک میکند و آماده رسم هر فریم میشویم. توجه کنید که ما باید گوشه چپ-بالای مستطیل را به صورت -(width/2), -(height/2) تعیین کنیم زیرا مبدأ را قبلاً به صورت width/2, height/2 تعیین کردهایم.
1ctx.fillRect(-(width/2), -(height/2), width, height);
سپس تصویر خود را با استفاده از drawImage رسم میکنیم. ما از نسخه 9 پارامتری آن استفاده میکنیم:
1ctx.drawImage(image, (sprite*102), 0, 102, 148, 0+posX, -74, 102, 148);
چنان که میبینید:
- ما یک image به عنوان تصویری که باید جاسازی شود تعیین میکنیم.
- پارامترهای 2 و 3 گوشه چپ-بالای قطعه برش یافته تصویر مبدأ را تعیین میکند، مقدار X به صورت sprite ضرب در 102 است که sprite شماره اسپریت بین 0 و 5 است و Y همواره برابر با صفر است.
- پارامترهای 4 و 5 اندازه قطعه برش یافته یعنی 102 در 148 پیکسل است.
- پارامترهای 6 و 7 گوشه چپ-بالای کادری که قطعه برش یافته در آن رسم میشود را تعیین میکنند. موقعیت X برابر با 0 + posX است یعنی میتوانیم موقعیت رسم را با عوض کردن مقدار posX جابجا کنیم.
- پارامترهای 8 و 9 اندازه تصویر روی بوم را تعیین میکند. ما میخواهیم اندازه اصلی آن را حفظ کنیم، بنابراین 102 و 108 را به ترتیب برای اندازه و ارتفاع وارد میکنیم.
اینک مقدار sprite را پیش از هر بار رسم عوض میکنیم. بلوک کد زیر را در انتهای تابع ()draw وارد کنید:
1if (posX % 13 === 0) {
2 if (sprite === 5) {
3 sprite = 0;
4 } else {
5 sprite++;
6 }
7 }
ما کل بلوک را در بخش زیر وارد میکنیم:
1if (posX% 13 === 0) { ... }
همچنین از عملگر پیمانه (%) برای بررسی این نکته که مقدار posX میتواند دقیقاً و بدون باقیمانده بر 13 تقسیم شود استفاده میکنیم. اگر چنین باشد با افزایش شماره sprite به اسپریت بعدی میرویم. این کار عملاً به این معنی است که ما تنها sprite را هر 13 فریم یک بار روی صفحه بهروزرسانی میکنیم یا به عبارت دیگر نرخ رفرش ما 5 فریم بر ثانیه است. توجه کنید که ()requestAnimationFrame در صورت امکان با نرخ 60 فریم بر ثانیه فراخوانی میشود. ما عامدانه نرخ فریم را کندتر کردهایم زیرا تنها شش اسپریت داریم که میتوانیم نمایش دهیم و اگر در هر ثانیه 60 بار اسپریت را عوض کنیم، کاراکتر ما بسیار به سرعت راه میرود.
درون بلوک بیرونی از یک گزاره if…else استفاده میکنیم تا بررسی کنیم آیا مقدار sprite روی 5 قرار دارد یا نه. اگر آخرین اسپریت را نمایش دهیم sprite را روی 0 ریست میکنیم و در غیر این صورت آن را 1 واحد افزایش میدهیم.
سپس باید شیوه تغییر مقدار posX را در هر فریم بررسی کنیم. بلوک کد زیر را درست زیر کد قبلی اضافه کنید:
1if(posX > width/2) {
2 newStartPos = -((width/2) + 102);
3 posX = Math.ceil(newStartPos / 13) * 13;
4 console.log(posX);
5 } else {
6 posX += 2;
7 }
ما از یک گزاره if ... else دیگر استفاده میکنیم تا ببینیم آیا مقدار posX بزرگتر از width/2 میشود یا نه. این بدان معنی است که کاراکتر ما از گوشه راست صفحه به بیرون میرود. اگر چنین باشد باید موقعیتی را محاسبه کنیم که کاراکتر را در سمت چپ، بخش چپ صفحه قرار میدهد و سپس posX را برابر با نزدیکترین ضریب 13 آن عدد قرار دهیم. این مقدار باید ضریبی از 13 باشد چون در غیر این صورت بلوک کد قبلی کار نمیکند، چون posX هرگز برابر با ضریب 13 نخواهد بود.
اگر کاراکتر انیمیشن هنوز از لبه صفحه بیرون نرفته است مقدار posX را به میزان 2 واحد افزایش میدهیم. بدین ترتیب در هر بار رسم، کمی به سمت راست حرکت میکند.
در نهایت باید حلقه انیمیشن را با فراخوانی requestAnimationFrame() در انتهای تابع draw() به اجرا درآوریم:
1window.requestAnimationFrame(draw);
بدین ترتیب و در نهایت مثال ما باید چیزی مانند زیر باشد:
نکته: کد نهایی این مثال به صورت زیر است:
1<!DOCTYPE html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <title>Canvas</title>
6 <style>
7 body {
8 margin: 0;
9 overflow: hidden;
10 }
11 </style>
12 </head>
13 <body>
14 <canvas class="myCanvas">
15 <p>Add suitable fallback here.</p>
16 </canvas>
17
18 <script>
19 var canvas = document.querySelector('.myCanvas');
20 var width = canvas.width = window.innerWidth;
21 var height = canvas.height = window.innerHeight;
22 var ctx = canvas.getContext('2d');
23 ctx.fillStyle = 'rgb(0,0,0)';
24 ctx.fillRect(0,0,width,height);
25 ctx.translate(width/2,height/2);
26 var image = new Image();
27 image.src = 'walk-right.png';
28 image.onload = draw;
29 var sprite = 0;
30 var posX = 0;
31 function draw() {
32 ctx.fillRect(-(width/2),-(height/2),width,height);
33 ctx.drawImage(image, (sprite*102), 0, 102, 148, 0+posX, -74, 102, 148);
34 if(posX % 13 === 0) {
35 if(sprite === 5) {
36 sprite = 0;
37 } else {
38 sprite++;
39 }
40 }
41 if(posX > width/2) {
42 newStartPos = -((width/2) + 102);
43 posX = Math.ceil(newStartPos / 13) * 13;
44 console.log(posX);
45 } else {
46 posX += 2;
47 }
48 window.requestAnimationFrame(draw);
49 };
50 </script>
51 </body>
52</html>
یک اپلیکیشن ساده برای رسم
به عنوان آخرین مثال این نوشته، یک اپلیکیشن بسیار ساده برای رسم معرفی میکنیم تا شیوه ترکیب حلقه انیمیشن را ورودی کاربر را نمایش دهیم. ما مراحل ساخت این مثال را مورد بررسی قرار نمیدهیم و صرفاً به بررسی بخشهای جالبتر کد میپردازیم.
کد این مثال به صورت زیر است:
1<!DOCTYPE html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <title>Canvas</title>
6 <style>
7 body {
8 margin: 0;
9 overflow: hidden;
10 background: #ccc;
11 }
12 .toolbar {
13 width: 150px;
14 height: 75px;
15 background: #ccc;
16 padding: 5px;
17 }
18 input[type="color"], button {
19 width: 90%;
20 margin: 0 auto;
21 display: block;
22 }
23 input[type="range"] {
24 width: 70%;
25 }
26 span {
27 position: relative;
28 bottom: 5px;
29 }
30 </style>
31 </head>
32 <body>
33 <div class="toolbar">
34 <input type="color" aria-label="select pen color">
35 <input type="range" min="2" max="50" value="30" aria-label="select pen size"><span class="output">30</span>
36 <button>Clear canvas</button>
37 </div>
38
39 <canvas class="myCanvas">
40 <p>Add suitable fallback here.</p>
41 </canvas>
42
43 <script>
44 var canvas = document.querySelector('.myCanvas');
45 var width = canvas.width = window.innerWidth;
46 var height = canvas.height = window.innerHeight-85;
47 var ctx = canvas.getContext('2d');
48 ctx.fillStyle = 'rgb(0,0,0)';
49 ctx.fillRect(0,0,width,height);
50 var colorPicker = document.querySelector('input[type="color"]');
51 var sizePicker = document.querySelector('input[type="range"]');
52 var output = document.querySelector('.output');
53 var clearBtn = document.querySelector('button');
54 // covert degrees to radians
55 function degToRad(degrees) {
56 return degrees * Math.PI / 180;
57 };
58 // update sizepicker output value
59 sizePicker.oninput = function() {
60 output.textContent = sizePicker.value;
61 }
62 // store mouse pointer coordinates, and whether the button is pressed
63 var curX;
64 var curY;
65 var pressed = false;
66 // update mouse pointer coordinates
67 document.onmousemove = function(e) {
68 curX = (window.Event) ? e.pageX : e.clientX + (document.documentElement.scrollLeft ? document.documentElement.scrollLeft : document.body.scrollLeft);
69 curY = (window.Event) ? e.pageY : e.clientY + (document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop);
70 }
71 canvas.onmousedown = function() {
72 pressed = true;
73 };
74 canvas.onmouseup = function() {
75 pressed = false;
76 }
77 clearBtn.onclick = function() {
78 ctx.fillStyle = 'rgb(0,0,0)';
79 ctx.fillRect(0,0,width,height);
80 }
81 function draw() {
82 if(pressed) {
83 ctx.fillStyle = colorPicker.value;
84 ctx.beginPath();
85 ctx.arc(curX, curY-85, sizePicker.value, degToRad(0), degToRad(360), false);
86 ctx.fill();
87 }
88 requestAnimationFrame(draw);
89 }
90 draw();
91 </script>
92 </body>
93</html>
در ادامه این مثال را در حالت اجرایی مشاهده میکنید:
در ادامه به بررسی بخشهای جالبتر کد میپردازیم. قبل از هر چیز رد مختصات X و Y ماوس و این که کلید ماوس کلیک شده یا نه را با متغیرهای curX ،curY و pressed حفظ میکنیم. زمانی که ماوس حرکت میکند یک مجموعه تابع به صورت دستگیره رویداد onmousemove اجرا میکنیم که مقادیر فعلی X و Y را دریافت میکند. همچنین از دستگیرههای رویداد onmousedown و onmouseup برای تغییر مقدار pressed به true در زمانی که دکمه ماوس زده میشود استفاده میکنیم و زمانی که کلیدش رها شود آن را مجدداً به حالت false تعیین میکنیم.
1var curX;
2var curY;
3var pressed = false;
4
5document.onmousemove = function(e) {
6 curX = (window.Event) ? e.pageX : e.clientX + (document.documentElement.scrollLeft ? document.documentElement.scrollLeft : document.body.scrollLeft);
7 curY = (window.Event) ? e.pageY : e.clientY + (document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop);
8}
9
10canvas.onmousedown = function() {
11 pressed = true;
12};
13
14canvas.onmouseup = function() {
15 pressed = false;
16}
زمانی که دکمه Clear canvas فشرده شود یک تابع ساده اجرا میکنیم که کل بوم را پاک میکند و به حالتی که قبلاً دیدیم درمیآید:
1clearBtn.onclick = function() {
2 ctx.fillStyle = 'rgb(0, 0, 0)';
3 ctx.fillRect(0, 0, width, height);
4}
حلقه رسم کاملاً ساده است، اگر مقدار pressed به صورت true باشد، یک دایره رسم میکنیم که دارای استایل fill برابر با مقداری است که از انتخابگر رنگ به دست آمده است و شعاع نیز بر برابر با مقداری است که در ورودی range ارائه شده است.
1function draw() {
2 if(pressed) {
3 ctx.fillStyle = colorPicker.value;
4 ctx.beginPath();
5 ctx.arc(curX, curY-85, sizePicker.value, degToRad(0), degToRad(360), false);
6 ctx.fill();
7 }
8
9 requestAnimationFrame(draw);
10}
11
12draw();
نکته: انواع <input> به صورت range و color تقریباً به خوبی روی همه مرورگرها پشتیبانی میشوند و تنها استثنا نسخههای زیر 10 اینترنت اکسپلورر است. البته سافاری نیز هنوز از color پشتیبانی نمیکند. اگر مرورگر شما از این ورودیها پشتیبانی نمیکند به صورت فیلدهای متنی ساده fall back میشوند و میتوانید مقادیر موردنظر رنگ/شماره را خودتان وارد کنید.
WebGL
اینک زمان آن رسیده است که بوم 2 بعدی را کنار بگذاریم و یک بررسی اجمالی در مورد بوم 3 بعدی داشته باشیم. محتوای بوم 3 بعدی با استفاده از WebGL API تعیین میشود که یک API کاملاً جدا از API بوم 2 بعدی است، گرچه هر دو روی عنصر <canvas> رندر میشوند.
WebGL بر مبنای زبان برنامهنویسی گرافیک OpenGL طراحی شده است و امکان ارتباط مستقیم با GPU رایانه را به دست میدهد. در این حالت، نوشتن WebGL خام به زبانهای سطح پایین مانند ++C نزدیکتر است تا جاوا اسکریپت و گرچه کاملاً پیچیده، اما بسیار قدرتمند است.
استفاده از یک کتابخانه
اغلب افراد کد گرافیکی 3 بعدی را به دلیل پیچیدگیاش با استفاده از یک کتابخانه جاوا اسکریپت شخص ثالث مانند Three.js، PlayCanvas یا Babylon.js انجام میدهند. اغلب آنها طرز کار مشابهی دارند و کارکردی برای ایجاد شکلهای ابتدایی و سفارشی، موقعیتیابی دوربینهای تماشا و ابزارهای نورپردازی، تبدیل سطوح به بافت و موارد دیگر ارائه میکنند. این کتابخانهها WebGL را برای شما مدیریت میکنند و امکان کار در سطوح بالاتر را فراهم میسازند.
استفاده از یکی از این ابزارها باعث میشود که API جدیدی را بیاموزید، ما بسیار سادهتر از کدنویسی WebGL خام است.
بازسازی مثال مکعب
در ادامه به مثال سادهای از شیوه ایجاد یک چیز جدید در کتابخانه WebGL میپردازیم. ما کتابخانه Three.js را انتخاب میکنیم، چون یکی از محبوبترین کتابخانهها محسوب میشود. در این راهنما یک مکعب چرخان 3 بعدی که قبلاً در ابتدای مطلب دیدیم را ایجاد میکنیم.
در آغاز کار یک کپی از فایل زیر در یک پوشه روی سیستم خود ایجاد کنید و سپس تصویر metal003.png (+) را دانلود کرده در همان پوشه قرار دهید. این همان تصویری است که به عنوان بافت سطح برای مکعب استفاده میکنیم.
فایل index.html
1<!DOCTYPE html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
6 <meta name="viewport" content="width=device-width">
7
8 <title>Three.js basic cube example</title>
9
10 <style>
11 html,body {
12 margin: 0;
13 }
14 body {
15 overflow: hidden;
16 }
17 </style>
18 </head>
19
20 <body>
21
22 <script src="three.min.js"></script>
23 <script src="main.js"></script>
24 </body>
25</html>
سپس فایل index.html را در ادیتور کد خود باز کنید تا ببینید که دو عنصر <script> دارد. عنصر اول برای الصاق three.min.js به صفحه و دومی برای الصاق فایل main.js به صفحه استفاده میشود. ما باید کتابخانه three.min.js (+) را دانلود کرده و در همان دایرکتوری قبلی ذخیره کنیم.
اینک three.js به صفحه الصاق یافته است و میتوانیم شروع به نوشتن کد جاوا اسکریپتی بکنیم که از آن در min.js استفاده میکند. کار خود را با ایجاد یک صحنه جدید آغاز میکنیم. کد زیر را به فایل main.js اضافه کنید:
1var scene = new THREE.Scene();
سازنده ()Scene یک صحنه جدید ایجاد میکند که نماینده کل دنیای 3 بعدی است که میخواهیم نمایش دهیم.
سپس باید یک camera داشته باشیم تا بتوانیم صحنه خود را ببینیم. دوربین در فرهنگ اصلاحات طراحی 3 بعدی، نماینده موقعیت بیننده در دنیا است. برای ایجاد یک دوربین باید خطوط زیر را به صفحه اضافه کنید:
1var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
2camera.position.z = 5;
سازنده ()PerspectiveCamera چهار آرگومان میگیرد:
- میدان دید (field of view) – میزان عریض بودن ناحیه پیش روی دوربین که باید روی صفحه دیده شود و بر حسب درجه است.
- نسبت ابعادی (aspect ratio) – معمولاً این نسبت برابر با عرض صحنه تقسیم بر ارتفاع صحنه است. استفاده از مقدار دیگر موجب اعوجاج صحنه میشود که شاید مطلوب شما باشد ولی عموماً چنین نیست.
- صفحه نزدیک (near plane) – میزان نزدیک بودن اشیا به دوربین را قبل از توقف رندر کردن آنها روی صفحه تعیین میکند. آن را میتوان با این موقعیت تصور کرد که انگشت خود را کمکم به چشمانتان نزدیک میکنید تا این که جایی بین چشمان شما قرار میگیرد که دیگر نمیتوانید آن را ببینید.
- صفحه دور (far plane) – میزان دور بودن اشیا از دوربین را که دیگر نمیتوانند رندر شوند تعیین میکند.
همچنین موقعیت دوربین را در مسافت 5 واحد از محور Z تعیین میکنیم که مانند CSS به سمت خارج از صفحه یعنی به سمت شما (بیننده) است.
عامل ضروری سوم رندر کننده است. این شیئی است که یک صحنه مفروض را چنان که توسط دوربین مفروض دیده میشود، رندر میکند. ما با استفاده از سازنده ()WebGLRenderer یک رندرکننده میسازیم، اما در حال حاضر از آن استفاده نمیکنیم. خطوط زیر را به کد قبلی اضافه کنید:
1var renderer = new THREE.WebGLRenderer();
2renderer.setSize(window.innerWidth, window.innerHeight);
3document.body.appendChild(renderer.domElement);
خط اول یک رندرکننده جدید میسازد، خط دوم اندازهای را تعیین میکند که رندرکننده با آن نمای دوربین را رسم خواهد کرد و خط سوم عنصر <canvas> ایجاد شده از سوی رندرکننده را به <body> سند الحاق میکند. اینک هر چیزی که رندرکننده رسم کند روی پنجره ما رسم میشود.
سپس میخواهیم مکعبی ایجاد کنیم که روی بوم نمایش پیدا کند. به این منظور بلوک کد زیر را به انتهای جاوا اسکریپت موجود اضافه کنید:
1var cube;
2
3var loader = new THREE.TextureLoader();
4
5loader.load( 'metal003.png', function (texture) {
6 texture.wrapS = THREE.RepeatWrapping;
7 texture.wrapT = THREE.RepeatWrapping;
8 texture.repeat.set(2, 2);
9
10 var geometry = new THREE.BoxGeometry(2.4, 2.4, 2.4);
11 var material = new THREE.MeshLambertMaterial( { map: texture, shading: THREE.FlatShading } );
12 cube = new THREE.Mesh(geometry, material);
13 scene.add(cube);
14
15 draw();
16});
کارهای زیادی در این کد انجام مییابد که در ادامه آنها را توضیح میدهیم:
ابتدا یک متغیر سراسری cube ایجاد میکنیم تا بتوانیم از هر جایی در کد به مکعب خود دسترسی داشته باشیم.
سپس یک شیء جدید TextureLoader ایجاد میکنیم و ()load را نیز روی آن فراخوانی میکنیم. ()load در این مثال دو پارامتر میگیرد (گرچه میتواند موارد بیشتری بگیرد) که یکی بافتی است که میخواهیم بارگذاری شود (فایل PNG) و دیگری تابعی است که هنگام بارگذاری شدن بافت اجرا خواهد شد.
درون این تابع از مشخصههای شیء texture برای تعیین این که میخواهیم یک تکرار 2 در 2 از تصویر پیرامون همه اضلاع مکعب را بپوشاند استفاده میکنیم. سپس یک شیء BoxGeometry و یک شیء جدید MeshLambertMaterial میسازیم و آنها را کنار یکدیگر قرار میدهیم تا یک mesh برای مکعب ایجاد شود. یک شیء به طور معمول نیازمند یک هندسه (شکل کلی) و یک جنس (نوع سطح) است.
در نهایت مکعب خود را به صحنه اضافه میکنیم و سپس تابع ()draw را فرامیخوانیم تا شروع به انیمیشن کند.
پیش از آن که ()draw را تعریف کنیم باید چند نور به صحنه اضافه کنیم تا همه چیز روشنتر شود. بنابراین بلوک زیر را به صفحه اضافه میکنیم:
1var light = new THREE.AmbientLight('rgb(255, 255, 255)'); // soft white light
2scene.add(light);
3
4var spotLight = new THREE.SpotLight('rgb(255, 255, 255)');
5spotLight.position.set( 100, 1000, 1000 );
6spotLight.castShadow = true;
7scene.add(spotLight);
شیء AmbientLight نوعی از نور نرم است که کل صحنه را کمی روشن میکند مانند آفتاب که موجب روشنایی زمین میشود. در سوی دیگر شیء AmbientLight یک شعاع نور جهتدار است که بیشتر شبیه نور چراغ قوه است.
در نهایت تابع ()draw را به انتهای کد میافزاییم:
1function draw() {
2 cube.rotation.x += 0.01;
3 cube.rotation.y += 0.01;
4 renderer.render(scene, camera);
5
6 requestAnimationFrame(draw);
7}
این کد کاملاً گویا است. در هر فریم، مکعب را کمی روی محورهای X و Y خود میچرخانیم و سپس صحنه را چنان که از سوی دوربین دیده میشود رندر میکنیم و در نهایت ()requestAnimationFrame را فرامیخوانیم تا رسم فریم بعدی را زمانبندی کند.
در ادامه میتوانید وضعیت نهایی مثالی که ساختیم را ملاحظه کنید:
نکته: کد کامل مثال فوق به صورت زیر است. اگر هر نوع خطایی داشتید میتوانید کد خودتان را با مثال زیر تطبیق دهید:
فایل index.html
1<!DOCTYPE html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
6 <meta name="viewport" content="width=device-width">
7
8 <title>Three.js basic cube example</title>
9
10 <style>
11 html,body {
12 margin: 0;
13 }
14 body {
15 overflow: hidden;
16 }
17 </style>
18 </head>
19
20 <body>
21
22 <script src="three.min.js"></script>
23 <script src="main.js"></script>
24 </body>
25</html>
فایل main.js
1var scene = new THREE.Scene();
2
3var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
4camera.position.z = 5;
5
6var renderer = new THREE.WebGLRenderer();
7renderer.setSize(window.innerWidth, window.innerHeight);
8document.body.appendChild(renderer.domElement);
9
10var cube;
11
12var loader = new THREE.TextureLoader();
13
14loader.load( 'metal003.png', function (texture) {
15 texture.wrapS = THREE.RepeatWrapping;
16 texture.wrapT = THREE.RepeatWrapping;
17 texture.repeat.set(2, 2);
18
19 var geometry = new THREE.BoxGeometry(2.4,2.4,2.4);
20 var material = new THREE.MeshLambertMaterial( { map: texture, shading: THREE.FlatShading } );
21 cube = new THREE.Mesh(geometry, material);
22 scene.add(cube);
23
24 draw();
25});
26
27var light = new THREE.AmbientLight('rgb(255,255,255)'); // soft white light
28scene.add(light);
29
30var spotLight = new THREE.SpotLight('rgb(255,255,255)');
31spotLight.position.set( 100, 1000, 1000 );
32spotLight.castShadow = true;
33scene.add(spotLight);
34
35function draw() {
36 cube.rotation.x += 0.01;
37 cube.rotation.y += 0.01;
38 renderer.render(scene, camera);
39
40 requestAnimationFrame(draw);
41}
سخن پایانی
بدین ترتیب اینک با مطالعه این مطلب نسبتاً بلند ایده مفیدی از مبانی برنامهنویسی گرافیکی با استفاده از Canvas و WebGL کسب کردهاید و میدانید که چه کارهایی میتوانید با API انجام دهید. همچنین ایده مناسبی از این که برای کسب اطلاعات بیشتر باید چه کار بکنید دارید.
برای مطالعه بخش بعدی این سری مقالات روی لینک زیر کلیک کنید:
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای JavaScript (جاوا اسکریپت)
- آموزش JavaScript ES6 (جاوا اسکریپت)
- مجموعه آموزشهای برنامهنویسی
- WebGL چیست؟ — آموزش وب جی ال — به زبان ساده و گام به گام
- ۱۱ ترفند بسیار کاربردی جاوا اسکریپت — به زبان ساده
- آموزش جاوا اسکریپت — مجموعه مقالات جامع وبلاگ فرادرس
- حلقه for در جاوا اسکریپت — از صفر تا صد + مثال و کد