در این قسمت از آموزش به معرفی شکلهای پایه و ویژگیهای آنها میپردازیم. امیدواریم آموزش پیشین را به خوبی آموخته باشید تا بتوانید این بخش را نیز به همان خوبی یاد بگیرید. توجه کنید که در این آموزش مواردی مشابه با آنچه در آموزش SVG آموختید بررسی خواهد شد، بنابراین به جای توضیحات اضافه به همان بخشها رجوع میکنیم.
مواردی که در این آموزش بررسی میشوند، متدهای پایه برای ایجاد ترسیمات در canvas هستند و یادگیری آنها بسیار مهم است. کد اولیهی کار ما، همانطور که در آموزش پیشین به آن اشاره کردیم، به صورت زیر است و در ادامه از تکرار آن میپرهیزیم:
<canvas id="cvs" style="border: 0.1em solid #111;"></canvas>
let cvs = document.getElementById("cvs"),
ctx = cvs.getContext("2d");
cvs.width = cvs.height = 500;
متدهای fillRect و strokeRect
پیش از آغاز به دو متد دیگر برای رسم مستطیل اشاره میکنیم. متدهای fillRect
و strokeRect
که به ترتیب یک مستطیل رنگشده، و یک مستطیل حاشیهدار به صفحه اضافه میکنند. ورودیهای این دو متد مشابه متد rect
و به صورت زیر است:
ctx.rect(x, y, width, height);
ctx.fillRect(x, y, width, height);
ctx.strokeRect(x, y, width, height);
خب تفاوت این دو متد با متد rect
چیست؟ اول اینکه این دو متد منتظر دستور رسم نمیمانند بلکه دستور رسم درون این متدها قرار گرفته. دوم اینکه مستطیلهایی که با این متدها رسم شوند، وارد حافظهی canvas یا همان شکل فعلی (current path) نمیشوند. در کد زیر بهتر متوجه این ویژگی میشوید:
ctx.fillStyle = "#F00"; /* RED */
ctx.fillRect(50, 50, 200, 300);
ctx.fillStyle = "#00F"; /* BLUE */
ctx.fill(); /* NOTHING HAPPENS */
در کد بالا ابتدا ویژگی fillStyle
به رنگ قرمز تعیین میشود و سپس با استفاده از متد fillRect
یک مستطیل قرمز در مختصات و اندازهی موردنظر رسم شد. حال ویژگی fillStyle
به رنگ آبی تعیین شد و متد fill
فراخوانی شد، اما نتیجهی کد فقط یک مربع قرمز در صفحه است! علت این است که متد fill
فقط شکل فعلی را رسم میکند، ولی از آنجایی که متد fillRect
درون شکل فعلی قرار نمیگیرد، پس متد fill
روی آن اثری نگذاشت.
متدهای مربوط به کمان
متد arc
متد arc
برای رسم دایره یا کمان با شعاع ثابت استفاده میشود. این متد پنج ورودی اصلی و یک ورودی اختیاری دارد که به ترتیب مختصات X مرکز، مختصات Y مرکز، شعاع R، زاویهی آغازین startAngle و زاویهی پایانی endAngle است. ورودی ششم که اختیاری است، تعیین میکند که آیا شکل در جهت عقربههای ساعت رسم شود یا برعکس؛ مقدار پیشفرض آن false
(یعنی در جهت عقربههای ساعت) است. متد arc در دو حالت خود به صورت زیر است:
ctx.arc(x, y, r, startAngle, endAngle);
ctx.arc(x, y, r, startAngle, endAngle, antiClockwise);
نقش startAngle و endAngle در این متد چیست؟ برای درک بهتر ابتدا یک ساعت را در ذهن خود مجسم کنید که عقربهی آن روی ساعت 3 قرار دارد. روی نوک این عقربه یک مداد متصل است و با حرکت عقربه خط کشیده میشود. ساعت 3 زاویهی صفر ماست. حال اگر به اندازهی 360 درجه (2π رادیان) بچرخیم، باز به همان نقطهی ساعت 3 برمیگردیم. حال در ذهن خود عقربه را از آغاز تا پایان حرکتش ببینید، مداد با حرکت عقربه چه شکلی رسم میکند؟ یک دایره! با فرض اینکه مرکز دایره در (250,250) قرار دارد و شعاع آن 50 است، کد معادل این حرکت به این صورت است (استفاده از متد stroke
فراموش نشود):
ctx.arc(250, 250, 50, 0, Math.PI*2);
همانطور که میبینید، زوایا باید بر حسب رادیان باشند. زاویهی آغازین که صفر بود به عنوان startAngle، و زاویهی پایانی که همان 2π بود به عنوان endAngle تعیین شد. حال بیایید یک نوع حرکت دیگر را تجسم کنیم: عقربه از ساعت 12 که همان 90 درجه (π½ رادیان) است، به ساعت 3 که همان صفر درجه (صفر رادیان) است میرود. شکل حاصل چیست؟ شکل حاصل میتواند دو نتیجه داشته باشد. اگر عقربه مستقیم به سمت ساعت 3 حرکت کند، یکچهارم دایره کشیده میشود، ولی اگر عقربه به سمت ساعت 9 برود و بعد از طی مسیر طولانیتر به ساعت 3 برسد، سهچهارم دایره کشیده میشود. شکل زیر به درک بهتر موضوع کمک میکند:
در تصویر بالا خط سبز نشانگر زاویهی آغاز یا startAngle و خط قرمز نشانگر زاویهی پایان یا endAngle است و همانطور که پیداست، اگر ورودی ششم را از false
به true
تغییر دهیم نتیجه به این شکل تغییر میکند. در حالت عادی شکل در جهت عقربههای ساعت رسم میشود ولی اگر به true
تغییر کند در خلاف جهت عقربههای ساعت رسم خواهد شد. حال این کد را اجرا کنید و سعی کنید درک بهتری از این موضوع به دست بیاورید. سعی کنید زوایا را تغییر داده و همینطور تفاوت دو حالت anitClockwise را نیز مشاهده کنید:
ctx.arc(250, 250, 200, Math.PI*3/2, 0, true);
ctx.lineWidth = 10;
ctx.stroke();
توجه کنید که برای رسم دایره لازم است متدهای fill
یا stroke
فراخوانی شوند و این یعنی متد arc
وارد شکل فعلی میشود. در واقع به جز متدهای fillRect
و strokeRect
تقریبا تمام متدهای دیگر در canvas به شکل فعلی اثر میگذارند و نیازی به بازگو کردن این مورد نیست.
متد ellipse
این متد نیاز به توضیح خاصی ندارد زیرا بیشتر آن با متد arc
یکسان است. این متد برای رسم کمان با شعاع متغیر (بیضیشکل) استفاده میشود. این متد شبیه متد arc
رفتار میکند، البته با یک تفاوت کوچک در ورودیهای آن:
ctx.ellipse(x, y, rx, ry, rotation, startAngle, endAngle, antiClockwise);
ورودی rx شعاع بیضی روی محور X، و ورودی ry شعاع روی محور Y است. همچنین ورودی rotation یک زاویه (به رادیان) است که چرخش کلی شکل را تعیین میکند. توجه کنید ورودی antiClockwise در اینجا هم اختیاری است. نکتهی آخر اینکه این متد جزو استاندارد اولیه نبوده و بعدها به آن اضافه شده و بهتر است نگاهی به جدول پشتیبانی آن بیاندازید.
متدهای مربوط به خط
اکنون میخواهیم متدهای مربوط به خط و منحنی را بررسی کنیم. تقریبا تمام این متدها نوع معادلی در عنصر SVG path دارند که در بخش آموزش SVG نیز دربارهی آنها توضیحاتی ارائه شده و میتوانید از آن توضیحات نیز برای درک بهتر استفاده کنید. همچنین در آموزشهای پیشرفتهتر آشنایی شما با اشکال SVG بهویژه عنصر path ضروری است بنابراین بهتر است از هماکنون به یادگیری آنها نیز بپردازید.
متدهای moveTo و lineTo
متد moveTo
معادل دستور M است. این متد دو ورودی میپذیرد که مختصات نقطهای هستند که میخواهیم قلم به آنجا حرکت کند. متد lineTo
نیز معادل دستور L است. این متد نیز دو ورودی میپذیرد که مختصات نقطهای هستند که میخواهیم خط (از جایی که قلم در آن قرار دارد) به آنجا کشیده شود. برای نمونه کد زیر یک مثلث رسم میکند:
ctx.moveTo(20, 20);
ctx.lineTo(20, 120);
ctx.lineTo(120, 120);
ctx.lineTo(20, 20);
ctx.lineWidth = 7;
ctx.stroke();
در کد بالا ابتدا قلم به مختصات (20,20) رفته، سپس به ترتیب به مختصات (20,120) و (120,120) خط رسم شده، و خط آخر به مختصات اولیه یعنی (20,20) بازمیگردد. در پایان اندازهی حاشیه برابر 7 قرار گرفته و حاشیه رسم میشود.
متد closePath
در کد بالا میتوانستیم به جای رسم خط به نقطهی اولیه، فقط از دستور closePath
استفاده کنیم تا شکل بسته شود. این متد معادل دستور Z است و هیچ ورودی نمیپذیرد. این متد یک خط از مکان فعلی قلم به مختصات آخرین دستور moveTo
رسم میکند. برای نمونه به کد زیر دقت کنید. در کد زیر دستور closePath
اول به مختصات (10,10) خط رسم میکند اما دستور دوم به مختصات (110,10) باید به این نکته در ترسیمات بزرگ و پیچیده دقت کنید:
ctx.moveTo(10, 10); /*-*-*/
ctx.lineTo(10, 100);
ctx.lineTo(100, 100);
ctx.closePath(); /* lineTo(10, 10) */
ctx.moveTo(110, 10); /*-*-*/
ctx.lineTo(110, 100);
ctx.lineTo(210, 100);
ctx.closePath(); /* lineTo(110, 10); */
ctx.lineWidth = 2;
ctx.stroke();
متد quadraticCurveTo
این متد معادل دستور Q است البته در اینجا خبری از دستور S همراه با آن نیست. این متد چهار ورودی میپذیرد که به ترتیب مختصات نقطهی کنترلکننده و مختصات نقطهی پایانی است. درست شبیه SVG، نقطهی اولیه همان نقطهای خواهد بود که قلم روی آن بود، که البته میتوان با متد moveTo
آن را تغییر داد. کد زیر این متد و کد معادل آن در SVG را نشان میدهد:
ctx.moveTo(30, 100);
ctx.quadraticCurveTo(60, 20, 90, 100);
/* <path d="M 10 100, Q 60 20, 90 100" /> */
در کد بالا نقطهی اولیه (30,100)، نقطهی کنترلکننده (60,20)، و نقطهی پایانی (90,100) است. توجه کنید که در کد معادل، تمام حروف بزرگ هستند و این یعنی در ترسیمات canvas مختصات نسبی وجود ندارد و تمام مختصات مطلق هستند.
توجه کنید که در ترسیمات canvas، مختصات نسبی وجود ندارد و تمام مختصات مطلق هستند.
متد bezierCurveTo
این متد نیز معادل دستور C است ولی دستور T بخشی از آن نیست. این متد شش ورودی میپذیرد که مختصات دو نقطهی کنترلکننده و نقطهی پایانی هستند. نقطهی اولیه را میتوان شبیه به متد قبل با استفاده از moveTo
تعیین کرد. در کد زیر یک نمونه منحنی همراه با کد معادل آن در SVG را نشان میدهد:
ctx.moveTo(20, 20);
ctx.bezierCurveTo(20, 80, 120, 80, 120, 20);
/* <path d="M 20 20, C 20 80, 120 80, 120 20" /> */
در کد بالا نقطهی اولیه (20,20)، دو نقطهی کنترلکننده (20,80) (120,80)، و نقطهی پایانی (120,20) است. برای درک بهتر آن، مختصات نقاط کنترلکننده را تغییر داده و تغییرات منحنی را مشاهده کنید. البته توجه کنید برای رسم شدن این منحنی نیز اجرای دستور stroke
ضروری است.
متد arcTo
کار این متد، رسم یک زاویه (کمان) مماس بر دو خط است. ممکن است با خود بگویید دو متد arc
و ellipse
میتوانند این کار را بهخوبی انجام دهند و نیازی به این متد نیست؛ اما سادگی این متد میتواند در بسیاری از موارد به ما کمک کند. یکی از دلایل اینکه این متد در بخش متدهای مربوط به کمان بررسی نشد این است که برای کار با آن نیاز به استفاده از متد moveTo
نیز هست و باید پیش از این با آن آشنا میشدید.
ابتدا لازم است دربارهی روش کار این متد توضیح دهیم. میدانیم که اگر دو خط غیرموازی داشته باشیم، میتوانیم یک دایره با شعاع دلخواه مماس بر هر دو خط رسم کنیم. برای درک بهتر این موضوع به شکل زیر توجه کنید:
همانطور که میبینید، دو خط بنفش و زرد غیرموازی هستند و میتوان یک دایره به شعاع دلخواه مماس بر هر دو رسم کرد. حال به روش کار این متد میپردازیم. این متد پنج ورودی میپذیرد. مختصات نقطهی میانی، مختصات نقطهی پایانی، و شعاع دایره. شبیه به متدهای قبل، مختصات نقطهی اولیه باید توسط متد moveTo
یا آخرین متد رسم تعیین شود. به کد زیر دقت کنید:
ctx.moveTo(x0, y0);
ctx.arcTo(x1, y1, x2, y2, r);
این متد دو خط فرضی را درنظر میگیرد؛ خط گذرا از نقطهی اولیه (x0,y0) و نقطهی میانی (x1,y1) که در شکل با رنگ زرد نشان داده شده، و خط گذرا از نقطهی میانی (x1,y1) و نقطهی پایانی (x2,y2) که به رنگ بنفش است. حال این متد کمانی به شعاع r و مماس به این دو خط رسم میکند. برای درک بهتر این مورد به تصویر بالا توجه کنید. در تصویر بالا نقطهی اولیه سبز، نقطهی میانی قرمز، و نقطهی پایانی آبی رنگ است.
نکتهی مهم اینکه این متد یک دایره رسم نمیکند، بلکه یک کمان از نقطهی تماس با خط بالا به خط پایین رسم میکند (همیشه کمان کوچکتر رسم میشود). درضمن، کمانی که رسم میشود ممکن است از هیچکدام از نقاط اولیه یا پایانی عبور نکند! همانطور که در شکل مشخص است این مورد بدیهیست.
برتری این متد نسبت به دیگر متدها در این است که نیازی به تعیین مرکز دایره نیست. هرچند این متد کاربردهای چندانی ندارد اما در جای مناسب میتواند بسیار کاربردی باشد. البته توجه کنید پشتیبانی مرورگرها از این متد کمی متفاوت است و ظاهرا مرورگر opera از آن پشتیبانی نمیکند؛ پس این موارد را نیز پیش از استفاده درنظر بگیرید.
ویژگیهای مربوط به خط
به جز متدهای رسم خط، ویژگیهایی نیز در canvas وجود دارند که میتوان با استفاده از آنها شکل خطوط را به دلخواه تغییر داد. اولین ویژگی مربوط به خط که بررسی کردیم ویژگی lineWidth
بود و اکنون به بررسی دیگر ویژگیها میپردازیم.
نکتهی مهم اینکه این ویژگیها فقط به خطوطی که درون شکل فعلی هستند اعمال میشوند یعنی اگر خطی در canvas رسم شده باشد ولی دیگر جزو شکل فعلی نباشد، تغییر نخواهد کرد.
ویژگی lineCap
این ویژگی شکل دو سر خطوط را مشخص میکند و سه مقدار به صورت زیر میپذیرد. مقدار “round” سر خطوط را گرد میکند. دو مقدار “butt” و “square” سر خطوط را تخت میکنند، با این تفاوت که ویژگی “butt” از مختصات تعیینشده برای خط فراتر نمیرود، اما ویژگی “square” به نسبت lineWidth
به ابتدا و انتهای خطوط میافزاید. برای درک بهتر این موضوع، به lineWidth
مقداری بزرگ بدهید و این دو ویژگی را روی یک خط امتحان کنید. همانطور که مشاهده میکنید، ویژگی “square” روی طول خطوط تاثیرگذار است. مقدار پیشفرض این ویژگی “butt” است.
ctx.lineCap = "round" || "butt" || "square";
ویژگی lineJoin
این ویژگی نوع اتصال دو خط را تعیین میکند و سه مقدار “bevel”، “round”، و “miter” میپذیرد و مقدار پیشفرض آن “miter” است. ویژگی “round” طبعا یک لبهی گرد ایجاد میکند، ویژگی “bevel” یک لبهی بریدهشده ایجاد میکند، و ویژگی “miter” یک لبهی تیز ایجاد میکند.
ctx.lineJoin = "miter" || "bevel" || "round";
ویژگی miterLimit
این ویژگی زمانی کاربرد دارد که ویژگی lineJoin
برابر “miter” باشد. مقدار پیشفرض آن برابر 10 است و یک محدوده برای لبهی تیز خطوط ایجاد میکند. به گونهای که اگر اندازهی این لبه از این مقدار کوچکتر بود، به صورت عادی رسم میشود، اما اگر اندازهی آن بزرگتر بود، به صورت “bevel” رسم میشود.
حالت خاص کمان
همانطور که متوجه شدهاید، متدهای مربوط به کمان نیز جزو متدهای مربوط به خط بودند اما آنها را جدا در نظر گرفتیم. به طور کلی در canvas فقط متد rect
یک شکل کامل رسم میکند و دیگر متدها فقط خط رسم میکنند. شکلهای دیگر همچون دایره، مثلث، چندضلعی و… همگی از ترکیب متدهای مربوط به خط ایجاد میشوند.
در سه متد arc
، ellipse
، و arcTo
بر خلاف دیگر متدهای رسم، مختصات نهایی قلم مشخص نیست. در دیگر متدهای رسم، مختصات نهایی قلم بخشی از نقاط ورودی است و بهراحتی میتوان مختصات نهایی قلم را تشخیص داد و ترسیمات را ادامه داد، اما این سه متد رفتار متفاوتی دارند و مختصات نهایی قلم در این سه متد به سادگی مشخص نمیشود.
دیگر ویژگی مهم دو متد arc
و ellipse
این است که هنگام فراخوانیشان، ابتدا خطی از مختصات فعلی قلم به زاویهی آغاز رسم میشود، و سپس کمان موردنظر رسم شده، و مختصات نهایی قلم در همان زاویهی پایانی قلم باقی میماند. برای درک بهتر موضوع به کد زیر دقت کنید:
ctx.arc(100, 100, 50, 0, Math.PI * 3/2);
ctx.arc(250, 100, 50, 0, Math.PI * 3/2);
ctx.lineWidth = 3;
ctx.stroke();
در کد بالا ابتدا سهچهارم یک دایره رسم میشود، سپس یک کمان دیگر در مختصاتی دیگر رسم میشود؛ اما کمان اول از زاویهی پایانی خود به زاویهی آغازی کمان دوم متصل شده. علت اینکه دو کمان به هم متصل شدهاند این است که اولا مختصات نهایی قلم روی زاویهی پایانی کمان اول بود، دوما هنگام رسم کمان دوم، ابتدا خطی از مختصات قلم (روی کمان اول) به زاویهی آغازی کمان دوم وصل شد، و سپس کمان دوم رسم شد. حال اگر یک کمان سوم هم به ترکیب اضافه کنیم چه اتفاقی میافتد؟ درست حدس زدید! انتهای کمان دوم به ابتدای کمان سوم وصل میشود.
چطور میتوان کاری کرد که چند کمان ناخواسته به همدیگر متصل نشوند؟ راههای بسیار زیادی برای این کار وجود دارد، به ویژه اگر بتوانید بهخوبی محیط canvas را کنترل کنید. سادهترین راه این است که با استفاده از متد moveTo
، قلم را به همان نقطهای ببریم که کمان شروع به رسم میکند. منظور مرکز کمان نیست، بلکه زاویهی آغازی آن است. مثلا وقتی کد مربوط به دو کمان را به این شکل اصلاح کنیم دیگر به یکدیگر متصل نمیشوند:
ctx.arc(100, 100, 50, 0, Math.PI * 3/2);
ctx.moveTo(300, 100); /* move to arc's beginning */
ctx.arc(250, 100, 50, 0, Math.PI * 3/2);
ctx.lineWidth = 3;
ctx.stroke();
همانطور که گفته شد راههای زیادی برای این کار وجود دارد که هرکدام برتریهای خود را دارند. در ادامه و در کار با موارد پیشرفته، بیشتر و بهتر به این موضوع خواهیم پرداخت.
ممکن است مخاطب تیزبین از خود بپرسد: «پس چرا در کد بالا، کمان اول بدون نیاز به متد moveTo
به جایی وصل نیست؟ مگر مختصات اولیهی قلم در مبدا مختصات (0,0) نیست؟» پاسخ این است که مختصات اولیهی قلم تا وقتی که یک شکل رسم نشود یا از متد moveTo
استفاده نشود، تعریفنشده باقی میماند. به همین دلیل است که کمان اول به جایی وصل نشد. ممکن است این مورد کمی برایتان گنگ باشد پس به این نمونه کد دقت کنید:
ctx.lineTo(100, 100); /* from (???, ???) to (100, 100) */
ctx.lineTo(200, 100); /* from (100, 100) to (200, 100) */
ctx.lineWidth = 3;
ctx.stroke();
در کد بالا، ابتدا یک متد lineTo
، بدون اینکه متد moveTo
پیش از آن اجرا شده باشد، اجرا میشود و یک خط از «مختصات اولیه» به مختصات (100,100) رسم میکند. سپس متد lineTo
دوم از مختصات فعلی قلم (100,100) یک خط به (200,100) رسم میکند. وقتی کد را اجرا کنید متوجه حضور فقط یک خط در صفحه میشوید. علت آن است که خط اول که فکر میکردیم باید از مبدا مختصات به مختصات (100,100) رسم شود، رسم نشده. چرا؟ چون مختصات اولیه در مبدا مختصات نیست، بلکه تعریف نشده است. مثل اینکه بگوییم یک خط از مختصات «ناکجا» به فلان نقطه رسم شود!
خب چرا خط دوم رسم شد؟ چون با وجود اینکه متد lineTo
اول نتوانست چیزی رسم کند، اما مختصات نهایی قلم را که همان ورودی خودش بود مشخص کرد و به همین دلیل متد lineTo
دوم توانست خط رسم کند. به شکل مشابه، چیزی به کمان اول وصل نشد اما خودش رسم شد و مختصات نهایی قلم را نیز تغییر داد.
نتیجهگیری
در این آموزش تقریبا تمام متدها و ویژگیهای رسم در canvas را بررسی کردیم و سعی کردیم به بهترین شکل آنها را به مخاطب عزیز بیاموزیم. خواهشمندیم برای یادگیری بهتر حتما آنها را تمرین کنید. شاید باور نکنید اما تمام ترسیمات بزرگ و پیچیدهای که ممکن است دیده باشید تنها ترکیبی از این ترسیمات ساده و ابتدایی هستند! در آموزش بعدی به متدهای کنترل محیط در canvas و ایجاد تغییر در شکل فعلی، لایهی ترسیمات، و رفتار canvas در انجام ترسیمات میپردازیم.
سوال داری؟ برو به پنل پرسش و پاسخ