در canvas نیز درست مانند SVG میتوان طیف رنگ ساخت، حاشیهها را خطچین کرد، الگو ساخت، و حتی بدون کمک CSS برای ترسیمات سایه ایجادکرد! در این آموزش به تکتک این موارد میپردازیم و ویژگیها و موارد ویژهی هرکدام را بررسی میکنیم. این آموزش نسبت به دیگر آموزشها کمی طولانیتر است اما میتوانید بدون هیچ مشکلی هرکدام از چهار موضوع را جداگانه بیاموزید.
خطچین (line-dash)
رسم خطچین در ابتدا بخشی از canvas نبود اما امروزه بخشی از استاندارد آن است و پشتیبانی خیلی خوبی دارد. برای رسم خطچین دو متد و یک ویژگی وجود دارد که به توضیح هر یک میپردازیم. همانطور که مشخص است، این ویژگی زمانی اعمال میشود که به شکل حاشیه داده شود.
متد setLineDash
برای ایجاد خطچین به یک جفت عدد نیاز داریم. طول خط، و فاصله از خط بعدی. اما canvas یک قدم فراتر گذاشته و به جای یک جفت عدد، یک آرایه از اعداد میپذیرد که این یعنی میتوان در canvas خطچینهای پیچیدهتر ایجاد کرد. طول این آرایه نیز هیچ محدودیتی ندارد. این آرایه باید به عنوان ورودی متد setLineDash
قرار بگیرد. این متد فقط همین ورودی را دریافت میکند. آرایهی پیشفرض برای خطچین، یک آرایهی خالی است. در نمونه کد زیر چند نمونه خطچین ساده و پیچیده ایجاد میشود:
ctx.setLineDash([20, 20]);
ctx.setLineDash([20, 10, 30, 20]);
ctx.setLineDash([10, 5, 10, 20, 30, 10]);
ctx.setLineDash([10, 5, 20, 10, 30, 15, 40, 20, 50, 25]);
متد getLineDash
این متد آرایهی خطچین را برمیگرداند و طبعا هیچ ورودیای نمیپذیرد. در مواردی لازم است از آرایهی فعلی خطچین برای ساختن آرایهی بعدی استفاده شود. در این موارد این متد میتواند به ما کمک کند. به نمونه کد زیر دقت کنید. در کد زیر چند نمونه آرایه برای خطچین تعیین میشود و سپس با استفاده از متد getLineDash
دوباره دریافت میشوند:
ctx.getLineDash(); /* [] - default value */
ctx.setLineDash([10, 20]);
ctx.getLineDash(); /* [10, 20] */
ctx.setLineDash([5, 10, 5, 20]);
ctx.getLineDash(); /* [5, 10, 5, 20] */
ctx.setLineDash([20, 10, 30]);
ctx.getLineDash(); /* [20, 10, 30, 20, 10, 30] (!) */
پرسش مهمی که با بررسی کد بالا پیش میآید این است که چرا متد getLineDash
برای آرایهی آخر با سه عضو، یک آرایهی شش عضوی برگرداند؟ پاسخ این است که اعداد درون آرایه به صورت جفتی برای خطچین استفاده میشوند. وقتی یک آرایه با طول فرد به خطچین بدهیم، عدد آخر درون آرایه تنها میماند بنابراین یک کپی از این آرایه به انتهای آن اضافه میشود تا طول آن زوج شود.
اگر به آرایهی شش عضوی در کد بالا دقت کنید، دقیقا همین اتفاق افتاده است. حال شما یک ترفند عالی برای ساختن خطچینهای باز هم پیچیدهتر در اختیار دارید! چون در نیمهی دوم آرایهای که ساخته شده، جای طول خطچین و فاصله از خطچین بعدی عوض شده؛ و این یعنی باز هم پیچیدگی بیشتر!
ویژگی lineDashOffset
این ویژگی فاصلهی اولین خطچین را از نقطهی آغازین حاشیه تعیین میکند. مقدار پیشفرض آن صفر است. از این ویژگی میتوان برای ایجاد انیمیشن روی خطچین استفاده کرد. این ویژگی میتواند هر مقدار مثبت یا منفیای داشته باشد، اما بنا بر حالت پلهای این ویژگی، اگر اندازهی آن از مجموع اعداد آرایهی خطچین بیشتر شود، به حالت قبل بازمیگردد. برای درک بهتر این موضوع به کد زیر دقت کنید:
ctx.setLineDash([10, 20]); /* 10 + 20 = 30 */
ctx.lineDashOffset = 35; /* 35 === 30 + 5 === 0 */
ctx.setLineDash([20]); /* 20 + 20 = 40 */
ctx.lineDashOffset = 41; /* 41 === 40 + 1 === 1 */
ctx.setLineDash([10, 5]); /* 10 + 5 = 15 */
ctx.lineDashOffset = -15; /* -15 === 0 */
در قسمت اول کد بالا، آرایهی خطچین [10,20]
است که مجموع اعداد آن 30 میشود. این یعنی ویژگی lineDashOffset
هر 30 واحد یک بار تکرار میشود. این یعنی میتوانیم به هر تعداد 30 که خواستیم از آن کم کنیم. برای مثال به جای نوشتن 35 برای این ویژگی، میتوانیم 30 واحد از آن کم کنیم و 5 بنویسیم؛ و همچنان نتیجهی یکسان بگیریم.
قسمت دوم نیز به همین صورت است. مجموع اعداد داخل آرایه 40 میشود و این یعنی ویژگی lineDashOffset
هر 40 واحد یک بار تکرار میشود. یعنی به جای نوشتن 41 میتوانیم 1 بنویسیم و همان نتیجه را بگیریم. در قسمت سوم این مقدار 15 است و این یعنی به جای نوشتن 15 برای ویژگی lineDashOffset
میتوانیم صفر بنویسیم و نتیجه تغییر نکند. در نمونه کد زیر تمام مقادیری که برای lineDashOffset
نوشته شدهاند نتیجهی یکسان دارند:
ctx.setLineDash([5, 10, 10, 20]); /* 5 + 10 + 10 + 20 = 45 */
ctx.lineDashOffset = 1;
ctx.lineDashOffset = 45 + 1; /* 46 */
ctx.lineDashOffset = -45 + 1; /* -44 */
ctx.lineDashOffset = 200000 * 45 + 1;
اگر بهخوبی متوجه «پلهای بودن» این ویژگی نشدهاید، اشکالی ندارد! در بخش انیمیشن حداقل یک مثال در رابطه با این ویژگی بررسی خواهیم کرد و امیدواریم در آنجا این ویژگی را درک کنید!
نکتهی مهمی که باید در ذهن داشته باشید این است که تمام ویژگیهای خط به ویژه lineWidth
و lineCap
روی خطچین اثر میگذارند. در نمونههایی که در پایان این بخش بررسی شدهاند این ویژگیها را امتحان کرده و نتیجه را ببینید.
اگر به یاد داشته باشید، گفتیم که خطچین روی رفتار متد isPointInStroke
اثر میگذارد. اگر برای شکل فعلی یک حاشیه تعریف شده باشد، این متد فقط وقتی مقدار true
بازمیگرداند که نقطه روی خطچینها قرار داشته باشد. برای درک بهتر موضوع کد زیر را بررسی کنید. این کد نسخهی متفاوتی از کد آموزش قبل است که به آن خطچین اضافه شده:
cvs.width = 700;
cvs.height = 400;
ctx.arc(350, 200, 150, 0, Math.PI * 2);
ctx.moveTo(600, 200);
ctx.arc(500, 200, 100, 0, Math.PI * 2);
ctx.moveTo(300, 200);
ctx.arc(200, 200, 100, 0, Math.PI * 2);
ctx.lineWidth = 10;
ctx.lineCap = "round";
ctx.setLineDash([25]);
ctx.strokeStyle = "#C00";
ctx.stroke();
function check_cursor (e) {
let box = cvs.getBoundingClientRect(),
x = e.clientX - box.left,
y = e.clientY - box.top;
if (ctx.isPointInStroke(x, y)) ctx.strokeStyle = "#0C0";
else ctx.strokeStyle = "#C00";
ctx.clearRect(0, 0, cvs.width, cvs.height);
ctx.stroke();
}
cvs.addEventListener("mousemove", check_cursor);
در کد زیر چند نمونهی مختلف خطچین همراه با نتایج آوره شدهاست. در ردیف دوم خطچینها همگی یکسان هستن و در ویژگی lineDashOffset
با یکدیگر تفاوت دارند. با اینکه در شکل زیر فقط از مستطیل استفاده شده، اما خطچین روی حاشیهی هر نوع شکلی اعمال میشود، حتی روی متن! سعی کنید این کد را تغییر دهید تا درک بهتری از موضوع به دست آورید. بهترین راه یادگیری، تمرین عملی است:
cvs.width = 910;
cvs.height = 620;
ctx.lineWidth = 3;
/* -- 1 -- */
ctx.setLineDash([20]);
ctx.strokeRect(40, 40, 250, 250);
/* -- 2 -- */
ctx.setLineDash([20, 10, 30, 10]);
ctx.strokeRect(330, 40, 250, 250);
/* -- 3 -- */
ctx.setLineDash([20, 10, 30]);
ctx.strokeRect(620, 40, 250, 250);
/* -- * -- */
ctx.setLineDash([60, 30]);
/* -- 4 -- */
ctx.lineDashOffset = 0;
ctx.strokeRect(40, 330, 250, 250);
/* -- 5 -- */
ctx.lineDashOffset = 20;
ctx.strokeRect(330, 330, 250, 250);
/* -- 6 -- */
ctx.lineDashOffset = 40;
ctx.strokeRect(620, 330, 250, 250);
الگو (pattern)
شبیه به SVG، از الگو میتوان به جای رنگ شکل یا رنگ حاشیه استفاده کرد. البته لازم است ابتدا الگوی موردنظر تعریف شود. برای تعریف الگو از متد createPattern
استفاده میشود. این متد دو ورودی میپذیرد که به ترتیب یک عنصر نمایشی برای الگو، و نوع تکرار الگو است. این متد یک شئ از نوع CanvasPattern
بازمیگرداند که میتواند به عنوان مقدار ویژگی fillStyle
یا strokeStyle
قرار بگیرد و به شکلی که رسم میشود اعمال شود:
let my_pattern = ctx.createPattern(pattern_img, pattern_repeat);
ورودی pattern_img
که یک عنصر نمایشی است درست شبیه به تصویر ورودی برای متد drawImage
است. این ورودی هر چیزی میتواند باشد اما باید پیش از فراخوانی این متد بارگیری شده باشد. تمام موارد مربوط به تصویر در اینجا نیز درست هستند و باید رعایت شوند. ورودی pattern_repeat
شبیه به ویژگی background-repeat است و نوع تکرار الگو را مشخص میکند. مقدار پیشفرض آن “repeat” است اما میتواند هرکدام از مقادیر “no-repeat”، “repeat-x” و “repeat-y” را نیز داشته باشد.
به نمونه کد زیر دقت کنید. در کد زیر یک canvas
دوم (شبیه به آنچه در رسم تصویر انجام دادیم) ایجاد میشود و از آن به عنوان الگوی canvas
اصلی استفاده کرده و یک مربع رسم میکنیم. بیشتر ساختار این کد شبیه به کد قبل است و نیاز به توضیح خاصی ندارد. سعی کنید ترسیمات canvas
دوم را تغییر دهید تا الگوی متفاوتی ایجاد کنید:
let cvs = document.getElementById("cvs"),
ctx = cvs.getContext("2d");
cvs.width = cvs.height = 500;
let pattern_image = (() => {
let c = document.createElement("canvas");
c.width = c.height = 50;
c = c.getContext("2d");
c.arc(25, 25, 25, 0, Math.PI * 2);
c.strokeStyle = "#32CD32"; /* GREEN */
c.lineWidth = 10;
c.stroke();
return c.canvas;
}) ();
let cvs_pattern = ctx.createPattern(pattern_image, "repeat");
ctx.fillStyle = cvs_pattern;
ctx.fillRect(0, 0, cvs.width, cvs.height);
طیف خط (Gradient)
در canvas سه نوع طیف رنگ وجود دارد. طیف رنگ خطی، طیف رنگ شعاعی و طیف رنگ مخروطی. روش تعریف و استفاده از این طیفها شبیه به الگو است، با این تفاوت که رنگهایشان باید پیش از استفاده تعریف شوند. درضمن، نمیتوان یک طیف رنگ یا الگو را تغییر داد و اصلاح کرد.
شبیه به الگو، طیف رنگ ساخته شده توسط یک زمینه، میتواند توسط دیگر زمینهها نیز استفاده شود. هنگام ساختن طیف رنگ، یک شئ از نوع CanvasGradient
برگردانده میشود که یک متد به نام addColorStop
دارد. با استفاده از این متد میتوان به طیف موردنظر رنگ اضافه کرد. طیف رنگهای مختلف، متدهای مختلف با تعداد ورودی مختلف دارند اما همگی این نوع شئ با این متد را برمیگردانند.
متد addColorStop
دو ورودی میپذیرد. ورودی اول یک عدد بین 0 و 1 است که مکان رنگ در طیف را مشخص میکند. ورودی دوم نیز رنگ موردنظر است. میتوان به هر طیف به تعداد دلخواه رنگ اضافه کرد فقط کافیست عدد ورودی بین 0 و 1 باشد. هنگام بررسی هر نوع طیف رنگ یک مثال نیز برای درک بهتر موضوع بررسی میشود؛ لطفا کدها را اجرا کنید تا با روش کار هرکدام بهتر آشنا شوید.
طیف رنگ خطی
طیف رنگ خطی یا linear-gradient توسط متد createLinearGradient
ساخته میشود. این متد چهار ورودی میپذیرد که نقاط آغاز و پایان طیف هستند. این دو نقطه جهت و اندازهی طیف رنگ را مشخص میکنند. به نمونه کد زیر توجه کنید. در کد زیر یک طیف رنگ خطی ساخته میشود که آغاز آن در مبدا مختصات و پایان آن در انتهای عنصر است. سپس دو رنگ زرد و سبز به آن اضافه میشود. برای درک بهتر نقاط آغاز و پایان طیف، ورودیهای متد را در کد زیر تغییر دهید و تغییرات در نتیجه را مشاهده کنید:
ctx.createLinearGradient(X1, Y1, X2, Y2);
let linear_gradient = ctx.createLinearGradient(0, 0, cvs.width, cvs.height);
linear_gradient.addColorStop(0, "#CC0"); /* YELLOW */
linear_gradient.addColorStop(1, "#3C3"); /* GREEN */
ctx.fillStyle = linear_gradient;
ctx.fillRect(0, 0, cvs.width, cvs.height);
طیف رنگ شعاعی
طیف رنگ شعاعی یا radial-gradient توسط متد createRadialGradient
ساخته میشود. این متد شش ورودی میپذیرد که به ترتیب مختصات مرکز و شعاع دایرهی آغاز و پایان هستند. برخلاف CSS، در canvas میتوان طیف رنگهایی ساخت که مختصات دایرههایشان متفاوت است. در کد زیر یک طیف رنگ شعاعی با سه رنگ قرمز، زرد، و سبز ساخته میشود. مختصات هر دو دایره در (250,250) قرار دارد. شعاع دایرهی کوچکتر 0 و شعاع دایرهی بزرگتر 300 است. میتوانید مختصات دایرهی پایانی را تغییر دهید تا تفاوت آن را با طیف رنگ شعاعی عادی ببینید:
ctx.createRadialGradient(X1, Y1, R1, X2, Y2, R2);
let radial_gradient = ctx.createRadialGradient(250, 250, 0, 250, 250, 300);
radial_gradient.addColorStop(0, "#F30"); /* RED */
radial_gradient.addColorStop(0.5, "#CC0"); /* YELLOW */
radial_gradient.addColorStop(1, "#3F3"); /* GREEN */
ctx.fillStyle = radial_gradient;
ctx.fillRect(0, 0, cvs.width, cvs.height);
طیف رنگ مخروطی
طیف رنگ مخروطی یا conic-gradient نوعی طیف رنگ جدید است که هنوز در مرحلهی آزمایشی قرار دارد و پشتیبانی از آن بسیار ضعیف است؛ اما در اینجا فقط به بررسی مختصری از آن میپردازیم. توجه کنید، از آنجایی که این ویژگی هنوز در مرحلهی آزمایشی قرار دارد، ممکن است در آینده رفتار یا ورودیهای متد آن تغییر کنند. این نوع طیف توسط متد createConicGradient
ساخته میشود که سه ورودی میپذیرد: زاویهی آغازین و مختصات مرکز.
به نمونه کد زیر دقت کنید. در کد زیر که شبیه به کد طیف رنگ خطی است، یک طیف رنگ مخروطی در مرکز canvas و با زاویهی آغازین 90 درجه (نصف π رادیان) ساخته شده و رنگهای زرد و سبز به آن اضافه میشوند. برای اجرای این کد نیاز به مرورگری دارید که از این متد پشتیبانی کند. پس نگاهی به جدول پشتیبانی آن بیاندازید:
ctx.createConicGradient(angle, X, Y);
let conic_gradient = ctx.createConicGradient(Math.PI / 2, cvs.width / 2, cvs.height / 2);
conic_gradient.addColorStop(0, "#CC0"); /* YELLOW */
conic_gradient.addColorStop(1, "#3C3"); /* GREEN */
ctx.fillStyle = conic_gradient;
ctx.fillRect(0, 0, cvs.width, cvs.height);
از آنجایی که امکان تغییر رنگ برخی از خطوط در یک شکل وجود ندارد، طیفهای رنگ میتوانند جایگزین مناسبی برای اینگونه موارد باشند. هرچند پویایی چندانی ندارند، اما از هیچ بهترند! در بخشهای آینده بیشتر با کاربرد طیفهای رنگ آشنا خواهید شد.
سایه (shadow)
درست شبیه به CSS، در canvas امکان ایجاد سایه وجود دارد؛ البته با این تفاوت که فقط میتوان یک سایه ایجاد کرد و مانند CSS نمیتوان چندین سایهی مختلف برای شکل تعریف کرد. سایه توسط چهار ویژگی تعیین میشود:
- ویژگی
shadowColor
: این ویژگی رنگ سایه را تعیین میکند. مقدار پیشفرض آنrgba(0,0,0,0)
است و هر نوع رنگی را (به جز طیف رنگ و الگو) میپذیرد. - ویژگی
shadowBlur
: این ویژگی مات بودن سایه را تعیین میکند. مقدار پیشفرض آن 0 است و هر مقدار مثبتی را میپذیرد. هر چه این مقدار بزرگتر باشد، میزان مات بودن سایه بیشتر خواهد بود. - ویژگی
shadowOffsetX
: این ویژگی فاصلهی سایه از شکل را در جهت محور Xها تعیین میکند. یعنی اگر مقدار آن 100 باشد، سایه 100 واحد به سمت راست شکل خواهد بود. مقدار پیشفرض این ویژگی 0 است. - ویژگی
shadowOffsetY
: این ویژگی فاصلهی سایه از شکل را در جهت محور Yها تعیین میکند. یعنی اگر مقدار آن 100 باشد، سایه 100 واحد پایینتر از شکل خواهد بود. مقدار پیشفرض این ویژگی 0 است.
هر شکلی که در canvas رسم میشود، دارای سایه است؛ اما به خاطر مقدارهای پیشفرض این ویژگیها، سایه دقیقا پشت شکل افتاده و به نظر میرسد که شکلهای رسمشده سایه ندارند. سایهها ویژگیهایی دارند که در ادامه به بررسی آنها میپردازیم.
به نمونه کد زیر دقت کنید. در کد زیر سایه به رنگ سبز، و در هر محور به فاصلهی 10 از شکل و به اندازهی 5 واحد مات است. رنگ خود شکل را نیز آبی میکنیم. برای یادگیری بیشتر و تسلط بهتر به موضوع، کد را تغییر داده و تغییرات را بررسی کنید:
ctx.shadowOffsetX = 10;
ctx.shadowOffsetY = 10;
ctx.shadowBlur = 5;
ctx.shadowColor = "#32CD32"; /* GREEN */
ctx.fillStyle = "#008DDE"; /* BLUE */
ctx.fillRect(100, 100, 200, 200);
به ازای هر پیکسلی که رسم میشود، یک سایه نیز رسم میشود، این یعنی سایهای که با متد stroke
رسم میشود، با سایهای که با متد fill
رسم میشود متفاوت است. همچنین، اگر یک تصویر دارای شفافیت باشد، بخشهای شفافش دارای سایه نخواهند بود، زیرا بخشهای شفاف رسم نمیشوند که سایه داشته باشند! در کد زیر یک canvas
با ترسیماتی درون خود ایجاد میشود، سپس در یک canvas
دیگر که دارای سایه برای ترسیمات است، رسم میشود. هنگام رسم، فقط به آن بخشهایی از تصویر سایه داده میشود که شفاف نیستند. کد را اجرا کرده و نتیجه را ببینید:
let cvs_img = (() => {
let c = document.createElement("canvas");
c.width = c.height = 200;
c = c.getContext("2d");
c.lineWidth = 20;
c.arc(100, 100, 90, 0, Math.PI * 2);
c.strokeStyle = "#CC0"; /* YELLOW */
c.stroke();
c.beginPath();
c.lineWidth = 10;
c.arc(100, 100, 50, 0, Math.PI * 2);
c.strokeStyle = "#F30"; /* RED */
c.stroke();
c.beginPath();
c.arc(100, 100, 20, 0, Math.PI * 2);
c.fillStyle = "#3C3"; /* GREEN */
c.fill();
return c.canvas;
}) ();
ctx.shadowOffsetX = ctx.shadowOffsetY = 10;
ctx.shadowBlur = 10;
ctx.drawImage(cvs_img, 100, 100, 200, 200);
در واقع این ویژگی نتیجهی یک ویژگی دیگر مربوط به سایههاست. اگر پیکسلی که در حال رسم شدن است دارای شفافیت باشد، سایه نیز همان شفافیت را خواهد داشت. برای درک بهتر موضوع کد زیر را درنظر بگیرید. در کد زیر ویژگی fillStyle
دارای شفافیت 0.1 است ولی سایه هیچ شفافیتی ندارد، اما همانطور که خواهید دید این شفافیت روی سایه نیز اثر میگذارد:
ctx.shadowBlur = 10;
ctx.shadowColor = "#000";
ctx.shadowOffsetX = ctx.shadowOffsetY = 20;
ctx.fillStyle = "rgba(255, 0, 255, 0.1)";
ctx.fillRect(100, 100, 200, 300);
این ویژگی باعث شده شفافیت رنگ مستقیما روی سایه اعمال شود. به این ترتیب یک پیکسل کاملا شفاف (یا دارای شفافیت 0) چه سایهای خواهد داشت؟ یک سایهی کاملا شفاف! یا سایهای که دیده نمیشود. به این ترتیب بخشهای شفاف تصویر یا شکل سایه ندارند.
سایهها بخشی از شکل فعلی نیستند اما وارد لایهی ترسیمات میشوند. درضمن تا زمانی که ویژگیهای مربوط به سایهها به مقدار پیشفرض خود بازنگردند، برای شکلهای جدید سایه رسم خواهد شد. نکتهی آخر اینکه رسم سایه به پردازش نسبتا بالایی نیاز دارد، بنابراین بهتر است از آن در برنامههای سنگین کمتر استفاده کنید تا سرعت برنامه پایین نیاید.
به جز موارد گفتهشده، باید اشارهای نیز به متد setShadow
داشته باشیم. این متد سالها پیش توسط مرورگرهای رده webkit پشتیبانی میشد اما مدتهاست که منقضی شده و استفاده از آن اصلا پیشنهاد نمیشود. برای راحتی کار با سایهها در بخش «شخصیسازی canvas» راههایی بررسی شده.
نتیجهگیری
در این بخش سعی کردیم چهار ویژگی مهم در canvas را مورد بررسی قرار داده و ویژگیهای خاص هرکدام را توضیح دهیم. ممکن است موارد گفتهشده کاربرد زیادی در ترسیمات نداشته باشند، اما در جای خود بسیار کاربری هستند و ترسیمات خیلی زیبایی میسازند، برای نمونه تصویر بالای پست با این ویژگیها ساخته شده. همچنین در یکی از آموزشهای پایانی کاربرد آنها را در عمل خواهید دید. در آموزش بعدی گذری کوتاه بر متن خواهیم داشت.
سوال داری؟ برو به پنل پرسش و پاسخ