متن در canvas

هرچند امکانات متن در SVG بیشتر هستند، اما باز هم canvas در نوشتن متن حرفی برای گفتن دارد! گاهی لازم است درون canvas چیزی نوشته شود. برای این موقعیت‌ها متد‌هایی وجود دارند که می‌توانند نیاز ما را برطرف کنند. در این آموزش به نوشتن متن در canvas می‌پردازیم.

متد fillText

با استفاده از این متد می‌توان متن مورد‌نظر را به صورت fill رسم کرد. این متد سه ورودی اصلی می‌پذیرد که به ترتیب متن مورد‌نظر، و مختصات رسم هستند. ورودی چهارم که اختیاری است طول متن را مشخص می‌کند. اگر طول متن بیشتر از این مقدار باشد، متن فشرده (scale) شده تا در این طول جا شود:


ctx.fillText("text", X, Y, max_width);

متد strokeText

این متد نیز درست شبیه به متد قبل سه ورودی اصلی و یک ورودی اختیاری دارد. این متد متن مورد‌نظر را به صورت stroke رسم می‌کند و طبعا هر چیزی که به ترسیمات stroke اثر بگذارد، به متنی که با این متد رسم شود نیز تاثیر می‌گذارد:


ctx.strokeText("text", X, Y, max_width);

متن‌هایی که رسم می‌شوند می‌توانند حاشیه، خط‌چین، سایه، طیف رنگ، الگو، و خلاصه تمام ویژگی‌های دیگر ترسیمات در canvas را داشته باشند. مشکل اصلی این است که متن هیچگاه وارد شکل فعلی نمی‌شود و همچنین تعیین مختصات متن در کنار دیگر ویژگی‌های مربوط به متن کمی دردسر‌ساز است.

ویژگی textAlign

این ویژگی رفتاری مشابه ویژگی text-align در CSS دارد و از تکرار مطالب آن خودداری می‌کنیم. این ویژگی مقدار‌های زیر را می‌پذیرد. مختصات X ورودی محل این ویژگی را تعیین کرده و متن نسبت به این خط رسم می‌شود:


ctx.textAlign = "start" || "end" || "left" || "right" || "middle";

مختصات متن نسبت به این ویژگی رسم می‌شود، یعنی مثلا اگر مقدار آن left باشد، متن در آن مختصات X آغاز می‌شود، ولی اگر مقدار آن right باشد، متن در آن مختصات X پایان می‌یابد. مقدار center نیز باعث می‌شود مختصات وسط متن در آن مختصات X قرار بگیرد. برای درک بهتر موضوع کد زیر را اجرا کنید:


cvs.width = 700;
cvs.height = 300;

ctx.moveTo(350, 0);
ctx.lineTo(350, 300);

ctx.strokeStyle = "#F00";
ctx.lineWidth = 3;
ctx.stroke();

ctx.font = "3em consolas";

ctx.textAlign = "left";
ctx.fillText("this is left aligned", 350, 60);

ctx.textAlign = "right";
ctx.fillText("this is right aligned", 350, 160);

ctx.textAlign = "center";
ctx.fillText("this is centered", 350, 260);

ویژگی font

این ویژگی نیز دقیقا مانند ویژگی font در CSS رفتار می‌کند، با این تفاوت که نیازی به تعیین line-height ندارد. در کد زیر مقدار کامل این ویژگی نوشته شده است اما همانطور که گفته شد می‌توانید از نوشتن line-height خودداری کنید. در برخی مرورگر‌ها این ویژگی خود‌به‌خود حذف می‌شود:


ctx.font = "[font-style] [font-variant] [font-weight] [font-size] / [line-height] [font-family]";

متد measureText

این متد یک ورودی از نوع متن می‌پذیرد و ویژگی‌های مختلف آن را اندازه‌گیری کرده و در یک شئ از نوع TextMetrics برمی‌گرداند. این متد می‌تواند ویژگی‌های زیادی از متن را اندازه‌گیری کند، اما فقط اندازه‌گیری طول متن به‌خوبی پشتیبانی می‌شود! این متد از ویژگی font تاثیر می‌پذیرد. به نمونه کد زیر توجه کنید:


let txt = "Hello World!";

ctx.font = "1em consolas";
ctx.measureText(txt); /* { width: 65.9765625 } */

ctx.font = "2em consolas";
ctx.measureText(txt); /* { width: 131.953125 } */

ctx.font = "2em monospace";
ctx.measureText(txt); /* { width: 107.211914 } */

ctx.font = "3em monospace";
ctx.measureText(txt); /* { width: 160.784729 } */

ctx.font = "3em 'times new roman'";
ctx.measureText(txt); /* { width: 158.378906 } */

ویژگی textBaseline

این ویژگی نیز خط مبنای متن را مشخص می‌کند که متن نسبت به آن رسم می‌شود. این ویژگی نیز شبیه به ویژگی vertical-align در CSS است اما برخی موارد آن نیاز به توضیح دارند. این ویژگی مقادیر زیر را می‌پذیرد:


ctx.textBaseline = "top" || "hanging" || "middle" ||
                   "alphabetical" || "ideographic" || "bottom";

محل این خطوط در تصویر زیر مشخص شده اما رفتار هر خط شاید واضح نباشد. مختصات این خط نسبت به متن مشخص نمی‌شود، بلکه مختصات متن نسبت به این خط مشخص می‌شود! و مختصات این خط نیز توسط ورودی متد رسم متن مشخص می‌شود. برای نمونه اگر مختصات ورودی (500,300) باشد، مختصات Y این خط نیز 300 خواهد بود، سپس با توجه به ویژگی textBaseline، موقعیت متن نسبت به این خط مشخص شده و رسم می‌شود. مثلا اگر این ویژگی برابر top باشد، متن زیر آن نوشته می‌شود! (به تصویر دقت کنید تا بهتر متوجه شوید)

canvas text baseline

بررسی یک نمونه

با توجه به موارد گفته شده، در این بخش یک نمونه بررسی می‌شود. خواهشمندیم کد‌های زیر را به‌خوبی بررسی کرده و یاد بگیرید زیرا این موارد جزو ساده‌ترین کد‌ها در ایجاد انیمیشن یا ترسیمات پیچیده هستند و باید از هم‌اکنون به یادگیری آن‌ها بپردازید تا بعد‌ها به مشکل برنخورید. برای هر بخش از کد‌ها توضیحات موردنیاز نیز آورده شده است:


<canvas id="cvs" style="border: 0.1em solid #111;"></canvas>


let cvs = document.getElementById("cvs"),
    ctx = cvs.getContext("2d");

cvs.width = 700;
cvs.height = 300;

/* (1) */
ctx.textAlign = "center";
ctx.textBaseline = "middle";

ctx.font = "900 160px 'tahoma'";
ctx.lineWidth = 10;

ctx.lineCap = ctx.lineJoin = "round";
ctx.setLineDash([60, 15, 80, 15]);

/* (2) */
let gradient = ctx.createRadialGradient(
      cvs.width / 2, cvs.height / 2, 0,
      cvs.width / 2, cvs.height / 2, 350
    );

gradient.addColorStop(0.0, "#08D");
gradient.addColorStop(0.2, "#08D");

gradient.addColorStop(0.2, "#0B5");
gradient.addColorStop(0.4, "#0B5");

gradient.addColorStop(0.4, "#FF0");
gradient.addColorStop(0.6, "#FF0");

gradient.addColorStop(0.6, "#F30");
gradient.addColorStop(0.8, "#F30");

gradient.addColorStop(0.8, "#C0C");
gradient.addColorStop(1.0, "#C0C");

    /* (3) */
let vertices = [
        180, -10, 180,  60, 130,  60, 130, 100,
        -10, 100, -10, 120, 160, 120, 160, 310,
        180, 310, 180, 215, 520, 215, 520, 310,
        540, 310, 540, 120, 710, 120, 710, 100,
        570, 100, 570, -10, 550, -10, 550,  60,
        200,  60, 200, -10
    ],
    
    /* (4) */
    offset_size = ctx.getLineDash().reduce((a, b) => a + b),
    dash_offset = 0;

/* (5) */
function draw_text () {
    /* (6) */
    ctx.clearRect(0, 0, cvs.width, cvs.height);
    ctx.lineDashOffset = dash_offset;
    
    /* (7) */
    ctx.lineWidth = 13;
    ctx.strokeStyle = "#111";
    ctx.stroke();
    ctx.strokeText("TEXT", cvs.width / 2, cvs.height / 2);
    
    /* (8) */
    ctx.lineWidth = 8;
    ctx.strokeStyle = gradient;
    ctx.stroke();
    ctx.strokeText("TEXT", cvs.width / 2, cvs.height / 2);
    
    /* (9) */
    dash_offset = ++dash_offset % offset_size;
}

/* (10) */
for (let i = 0, l = vertices.length; i < l; i += 2) {
    ctx.lineTo(vertices[i], vertices[i + 1]);
}

/* (11) */
setInterval(draw_text, 16);

canvas dash-line animation

بخش 1 تعریف ویژگی‌های اولیه

در این بخش ویژگی‌های اولیه، از جمله font، lineJoin، lineCap، و خط‌چین را تعیین می‌کنیم. از آنجایی که می‌خواهیم متن در وسط عنصر cvs باشد، برای راحتی کار ویژگی‌های textAlign و textBaseline را به ترتیب به center و middle تعیین می‌کنیم.

بخش 2 تعریف طیف رنگ

در این بخش یک طیف رنگ شعاعی تعریف می‌کنیم که مرکز هر دو دایره‌ی آن وسط cvs بوده و شعاع دایره‌ی بزرگ‌تر نیز 350 است. رنگ‌های این طیف به گونه‌ای هستند که به آرامی تغییر نمی‌کنند بلکه دارای لبه‌های مشخص هستند. روش ساخت این نوع طیف رنگ در این پست به‌خوبی توضیح داده شده است.

بخش 3 تعریف مختصات نقاط شکل

در این بخش یک آرایه به نام vertices تعریف می‌شود. این آرایه شامل نقاطی است که می‌خواهیم به آن‌ها خط رسم کنیم. می‌توانستیم این کار را به صورت دستی انجام داده و صد‌ها خط کد تکراری بنویسیم اما با این ترفند به سادگی می‌توانیم هر شکل بزرگ و پیچیده‌ای را در چند خط ساده رسم کنیم.

بخش 4 مجموع اعضای آرایه‌ی خط‌چین

در این بخش با کمک متد reduce که یک متد ویژه‌ی آرایه است، مجموع اعداد درون آرایه‌ی خط‌چین را به دست آورده و در متغیر offset_size ذخیره می‌کنیم. درضمن یک متغیر به نام dash_offset تعریف می‌کنیم. قرار است برای خط‌چین شکل و متن یک انیمیشن ایجاد کنیم و برای این کار به این متغیر‌ها نیاز داریم.

بخش 5 تابع انیمیشن

در این بخش تابع draw_text را تعریف می‌کنیم که تابع اصلی ما در اجرای انیمیشن است. خود تابع نیاز به توضیحات خاصی ندارد و به توضیح کد درون آن می‌پردازیم.

بخش 6 آماده‌سازی اولیه

در این بخش لایه‌ی ترسیمات کاملا پاک می‌شود تا ترسیمات بعدی روی آن رسم شوند. همچنین، ویژگی lineDashOffset برابر مقدار متغیر dash_offset قرار می‌گیرد. این ویژگی تا پایان اجرای یک فریم از انیمیشن بدون تغییر باقی می‌ماند.

بخش 7 رسم لایه‌ی بیرونی

در این بخش رنگ حاشیه به سیاه و اندازه‌ی حاشیه به 13 تعیین می‌شود و متن و شکل رسم می‌شوند. توجه کنید که منظور از شکل، شکلی است که از رسم نقاط درون آرایه‌ی vertices ایجاد شده. این شکل یک بار تعریف شده ولی تا وقتی که بخواهیم درون شکل فعلی باقی می‌ماند.

بخش 8 رسم لایه‌ی اصلی

در این بخش رنگ حاشیه به طیف رنگ gradient که پیش‌تر تعریف کردیم و اندازه‌ی حاشیه به 8 تعیین می‌شود. این بار شکل و متن، با حاشیه‌ی کوچکتر و رنگ متفاوت روی حاشیه‌ای که در بخش قبل رسم شد، رسم می‌شوند و این توهم را به وجود می‌آورند که انگار حاشیه‌ی شکل، خود دارای حاشیه شده!

بخش 9 ایجاد انیمیشن

در این بخش یک واحد به متغیر dash_offset اضافه می‌شود؛ البته به واسطه‌ی عملگر % و متغیر offset_width، اطمینان حاصل می‌شود که مقدار آن از مجموع اعضای آرایه‌ی خط‌چین بیشتر نشود. این مورد ضروری نیست اما بنا به خاصیت پله‌ای خط‌چین، می‌توانیم از بزرگ شدن بی‌مورد آن جلوگیری کنیم و همان نتیجه را بگیریم. حال که یک واحد به این متغیر اضافه شده، در فریم بعدی، مقدار آن به lineDashOffset رسیده و باعث ایجاد انیمیشن روی خط‌چین می‌شود.

بخش 10 رسم خودکار خطوط

در این بخش از آرایه‌ی vertices استفاده می‌کنیم. در این بخش یک حلقه داریم که اعداد درون آرایه را به صورت جفتی انتخاب کرده و در متد lineTo قرار می‌دهد. به این ترتیب شکل موردنظر رسم می‌شود. منظور از انتخاب به صورت جفتی این است که گام حلقه 2 است و در هر بار اجرای حلقه می‌توانیم به عضو i و i+1 ام دسترسی داشته باشیم.

بخش 11 اجرای انیمیشن

در این بخش به کمک تابع setInterval یک انیمیشن ایجاد می‌شود که هر 16 میلی‌ثانیه یک بار اجرا می‌شود. هر 16 میلی‌ثانیه یک بار، تابع draw_text اجرا می‌شود؛ یعنی حدود 60 بار در یک ثانیه. با هر بار اجرای این تابع، یک واحد به dash_offset اضافه می‌شود و به این ترتیب حاشیه‌ی شکل دارای انیمیشن می‌شود.

تابع setInterval برای ایجاد انیمیشن مناسب نیست و در اینجا فقط از جهت آشنایی استفاده شده. در بخش «انیمیشن در canvas» مفصل به این موضوع پرداخته و توابع مناسب را بررسی می‌کنیم.

توضیحات تکمیلی

از جمله مواردی که ممکن است در کد بالا شما را گیج کرده باشند، می‌توان به عملگر % اشاره کرد. این عملگر باقی‌مانده‌ی تقسیم عدد سمت چپ را نسبت به عدد راست برمی‌گرداند. از خواص تقسیم این است که باقی‌مانده‌ی یک تقسیم هیچ‌گاه نمی‌تواند بزرگ‌تر از مقسوم‌علیه آن تقسیم (یعنی عدد سمت راست) بشود. ما از این ویژگی استفاده کردیم و باقی‌مانده‌ی تقسیم dash_offset بر offset_width را در خود متغیر dash_offset ذخیره کردیم. یعنی offset_width مقسوم‌علیه یا مخرج کسر ماست بنابراین عدد نهایی که باقی‌مانده‌ی تقسیم است، هیچ‌گاه از آن بزرگ‌تر نمی‌شود. این ویژگی تقسیم باعث شده نه‌تنها در اینجا، بلکه در پروژه‌های فراوان دیگری نیز از این عملگر برای هدف مشابه استفاده شود. ویژگی این عملگر، به‌ویژه در کار با آرایه‌ها، بسیار کاربردی است.

مشکلات کار با متن در canvas

متاسفانه canvas و SVG امکانات چندان مناسبی برای کار با متن ندارند. در واقع هدف اصلی این فناوری‌ها کار با متن نبوده و نیست. علاوه بر اینکه نوشتن متن به پردازش زیادی نیاز دارد، حتی متن مورد‌نظر وارد شکل فعلی نمی‌شود و این موضوع باعث شده که انجام بسیاری از کار‌ها یا خیلی سخت شده و یا غیرممکن باشد!

با اینکه می‌توان کار‌های جالبی با متن انجام داد، اما در موارد کاربردی‌تر مانند نمایش یک بند نوشته کار بسیار سختی خواهد بود و بهتر است به دنبال روش‌های جایگزین باشیم. در بخش «ترکیب HTML و canvas» به این موضوع خواهیم پرداخت.

نتیجه‌گیری

در این بخش سعی کردیم با امکانات مربوط به متن آشنا شده و یک نمونه‌ی نسبتا بزرگ نیز بررسی کردیم. سعی کنید با مواردی که در این آموزش یاد گرفتید یک طرح ایجاد کنید تا درک بهتری از روند طراحی و متد‌های canvas به دست آورید زیرا آموزش‌های آینده سخت‌تر و پیچیده‌تر خواهند بود. در آموزش آینده به «تعامل شکل فعلی و لایه‌ی ترسیمات» می‌پردازیم.

حسین رفیعی

حسین رفیعی

طراحی و برنامه‌نویسی رو از وب شروع کردم و مثل خیلی از شماها آموزش‌های آقای سیدی خیلی بهم کمک کرد. هرچند این روزا تمرکز من روی برنامه‌نویسی خارج از وب هست ولی هنوز هم توی این فضا هستم و امیدوارم بتونم به بقیه کمک کنم!

سوال داری؟ برو به پنل پرسش و پاسخ

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *