تعیین اندازه canvas و تعامل آن با CSS

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

برای سادگی کار و انجام درست آزمایش‌هایمان، برنامه‌ی زیر را در canvas اجرا می‌کنیم. در این برنامه، با هر کلیک کاربر روی عنصر، یک دایره در آن مختصات با رنگ تصادفی رسم می‌شود. شعاع دایره ثابت و برابر 50 است:


function random_color () {
    return `hsl(${~~(Math.random() * 360)}, 100%, 50%)`;
}

function add_arc (e) {
    let box = cvs.getBoundingClientRect(),

        x = e.clientX - box.left,
        y = e.clientY - box.top;
    
    ctx.save();
    ctx.fillStyle = random_color();

    ctx.translate(x, y);
    ctx.fill(main_arc);

    ctx.restore();
}

let main_arc = new Path2D;
main_arc.arc(0, 0, 50, 0, Math.PI * 2);

cvs.addEventListener("click", add_arc);

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

وقتی ویژگی‌های width و height یک عنصر canvas را تعیین می‌کنیم، یک تصویر با این اندازه‌ها ایجاد می‌کنیم؛ و وقتی با CSS این ویژگی‌ها را تعیین می‌کنیم، این تصویر به ابعاد موردنظر تغییر اندازه (scale) می‌دهد. برای درک بهتر موضوع کد زیر را اجرا کنید. در این کد اندازه‌ی اصلی عنصر cvs به 700 ولی ابعاد آن در CSS متفاوت تعیین شده‌اند:


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

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

#cvs {
    width: 600px;
    height: 200px;
}

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

تعیین اندازه‌ی عنصر canvas

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


#cvs {
    width: 60em;
    height: 40em;
    border: 0.2em solid #111;
    
    position: absolute;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
}

استفاده از clientWIdth و clientHeight

یکی از آسان‌ترین راه‌ها برای دریافت اندازه‌های تعیین‌شده برای عنصر، استفاده از ویژگی‌های clientWidth و clientHeight است. این ویژگی‌ها روی هر عنصری در سند HTML وجود دارند. این اندازه‌ها همیشه عدد صحیح هستند. به کد زیر دقت کنید. در کد زیر به کمک این ویژگی‌ها اندازه‌ی عنصر cvs را تعیین می‌کنیم:


function size_setup () {
    let w = cvs.clientWidth,
        h = cvs.clientHeight;
    
    cvs.width = w;
    cvs.height = h;
}

onload = size_setup;

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

استفاده از ClientRect

روش دیگر استفاده از متد getBoundingClientRect است. پیش از این استفاده از این متد را دیده‌اید. این روش کمی دقیق‌تر است، زیرا مقدار‌های خروجی از این متد عدد صحیح نیستند و می‌توانند بخش اعشاری نیز داشته باشند. به کد زیر دقت کنید. در کد زیر به کمک این متد اندازه‌ی عنصر cvs به دست می‌آید:


function size_setup () {
    let { width, height } = cvs.getBoundingClientRect();
    
    cvs.width = Math.round(width);
    cvs.height = Math.round(height);
}

onload = size_setup;

علت اینکه در کد بالا از Math.round استفاده شد، این است که اندازه‌های canvas باید اعداد صحیح باشند. می‌توانید به جای تابع round از توابع ceil و floor و… نیز استفاده کنید.

به جز این دو روش، روش‌های دقیق‌تر دیگری نیز مانند Resize Observer وجود دارند، اما در مسائل مربوط به فناوری canvas دو‌بعدی، استفاده از یکی از دو روش بالا کفایت می‌کند. روش‌های دقیق‌تر برای موارد مهم‌تر مانند WebGL کاربرد دارند.  خب همانطور که می‌بینید، مشکل تعیین اندازه در canvas حل شد! اما مشکل بزرگ‌تری هست که حل کردن آن دردسر بیشتری دارد.

افت کیفیت canvas

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

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

ویژگی devicePixelRatio

این ویژگی نسبت اندازه‌ی پیکسل CSS را به پیکسل نمایشگر به ما می‌دهد. به کمک این ویژگی می‌توانیم از افت کیفیت ترسیمات جلوگیری کنیم. به کد زیر دقت کنید. در کد زیر از این ویژگی برای تعیین اندازه‌ی درست عنصر استفاده شده است:


function size_setup () {
    let w = cvs.clientWidth,
        h = cvs.clientHeight,
        ratio = devicePixelRatio || 1;
    
    cvs.width = Math.round(w * ratio);
    cvs.height = Math.round(h * ratio);
}

هنگام استفاده از این ویژگی نیز باید از تابع round یا توابع دیگر استفاده کنیم، زیرا این مقدار معمولا عدد صحیح نیست. کد را اجرا کرده و نتیجه را ببینید. اگر نمایشگر شما معمولی باشد، تغییری مشاهده نخواهید کرد، پس می‌توانید به صورت دستی مقدار ratio را به 2 یا بیشتر تغییر دهید. حال کد را اجرا کرده و نتیجه را ببینید.

کد بالا توانست مشکل کیفیت را حل کند، اما دو مشکل دیگر ایجاد کرد، و اکنون باید به دنبال راه‌حل این مشکلات باشیم! مشکل اول اینکه اندازه‌ی ترسیمات به نسبت این ویژگی کوچک‌تر شده‌اند. این یعنی در دو نمایشگر با کیفیت متفاوت، اندازه‌ی ترسیمات نیز متفاوت خواهند بود! برای حل این مشکل لازم است ترسیمات را به اندازه‌ی devicePixelRatio تغییر اندازه بدهیم. کد بالا را به صورت زیر اصلاح کرده و نتیجه را ببینید:


function size_setup () {
    let w = cvs.clientWidth,
        h = cvs.clientHeight,
        ratio = devicePixelRatio || 1;
    
    cvs.width = Math.round(w * ratio);
    cvs.height = Math.round(h * ratio);

    ctx.scale(ratio, ratio);
}

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

در این حالت جدید، ترسیمات canvas در واحد پیکسل CSS رسم می‌شوند، ولی اندازه‌ی عنصر دیگر در این واحد نیست. برای درک بهتر این مشکل، کد زیر را اجرا کنید. در کد زیر، تعداد 100 دایره در مختصات تصادفی درون canvas رسم می‌شوند:


let main_arc = new Path2D, i, x, y;
main_arc.arc(0, 0, 50, 0, Math.PI * 2);

function draw_arcs () {
    for (i = 0; i < 100; i++) {
        x = random(0, cvs.width);
        y = random(0, cvs.height);

        ctx.save();
        ctx.fillStyle = random_color();
        ctx.translate(x, y);

        ctx.fill(main_arc);
        ctx.restore();
    }
}

onload = () => {
    size_setup();
    draw_arcs();
}

با دقت به نتیجه‌ی این کد متوجه می‌شوید که تعدادی از دایره‌ها، خارج از لایه‌ی ترسیمات رسم شده‌اند و دیده نمی‌شوند. مشکل از کجاست؟ مشکل این است که اندازه‌ی محدوده‌ی فعلی ترسیمات با ابعاد عنصر canvas همخوانی ندارد. در کد بالا از ابعاد خود عنصر canvas درون تابع random استفاده کرده‌ایم، در حالی که این ابعاد از محدوده‌ی فعلی ترسیمات فراتر می‌روند.

این موضوع شاید در برنامه‌ی بالا مشکل خاصی به حساب نیاید، ولی در برخی برنامه‌ها می‌تواند رفتار برنامه را به‌کلی خراب کند و مشکلات زیادی ایجاد کند. راه‌حل این مشکل چیست؟ می‌توانیم ابعاد محدوده‌ی ترسیمات عنصر را جداگانه ذخیره کرده و هنگام نیاز استفاده کنیم. به کد زیر دقت کنید. در کد زیر متغیر area تعریف شده و اندازه‌ی محدوده‌ی ترسیمات عنصر cvs را ذخیره می‌کند:


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

    area = { w: null, h: null };

function size_setup () {
    let w = cvs.clientWidth,
        h = cvs.clientHeight,
        ratio = devicePixelRatio || 1;
    
    cvs.width = Math.round(w * ratio);
    cvs.height = Math.round(h * ratio);

    area.w = w;
    area.h = h;

    ctx.scale(ratio, ratio);
}

طبق تعریفات بالا، از این به بعد متغیر area اندازه‌های اصلی عنصر cvs را درون خود دارد و باید از این اندازه‌ها برای ترسیمات استفاده کنیم. با اصلاح کد رسم دایره‌ها به شکل زیر خواهید دید که مشکل ما نیز حل خواهد شد و تمام دایره‌ها در محدوده‌ی ترسیمات رسم خواهند شد:


function draw_arcs () {
    for (i = 0; i < 100; i++) {
        x = random(0, area.w);
        y = random(0, area.h);

        ctx.save();
        ctx.fillStyle = random_color();

        ctx.translate(x, y);
        ctx.fill(main_arc);

        ctx.restore();
    }
}

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


function size_setup () {
    let w = cvs.clientWidth,
        h = cvs.clientHeight,
        ratio = devicePixelRatio || 1;
    
    cvs.width = Math.round(w * ratio);
    cvs.height = Math.round(h * ratio);

    cvs.area = { w, h };
    ctx.scale(ratio, ratio);
}

خب ظاهرا دیگر مشکلی وجود ندارد، اما اشتباه می‌کنید! در یک صفحه وب معمولا کاربران صفحه را بزرگنمایی می‌کنند، درضمن در برخی برنامه‌های canvas باید امکان تغییر اندازه‌ی عنصر نیز وجود داشته باشد. شاید در ابتدا مشکلی در این مورد وجود نداشته باشد، اما کافیست ویژگی‌های CSS زیر را به عنصر اعمال کرده و پس از اجرای برنامه، صفحه را بزرگنمایی کنید:


#cvs {
    width: 60em;
    height: 40em;
    border: 0.2em solid #111;

    position: absolute;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
}

در ابتدای اجرای برنامه، مشکلی وجود ندارد، ولی پس از بزرگنمایی، نه‌تنها رفتار برنامه خراب می‌شود، بلکه کیفیت ترسیمات نیز پایین می‌آید. علت این مشکل چیست؟ باز هم تعریف واحد پیکسل CSS! با بزرگنمایی صفحه، اندازه‌ی هر پیکسل CSS نیز تغییر می‌کند و به طبع آن، ویژگی devicePiexlRatio نیز تغییر می‌کند. در وضعیت فعلی، با بزرگنمایی صفحه، اندازه‌ی عنصر ثابت باقی می‌ماند و به همین دلیل کیفیت ترسیمات دچار افت می‌شود.

برای حل این مشکل باید با هر بار تغییر اندازه‌ی صفحه، که بزرگنمایی هم شامل آن می شود، اندازه‌ی عنصر canvas را نیز مثل قبل تعیین کنیم. یعنی کد اصلی را باید به این شکل اصلاح کنیم:


function size_setup () {
    let w = cvs.clientWidth,
        h = cvs.clientHeight,
        ratio = devicePixelRatio || 1;
    
    cvs.width = Math.round(w * ratio);
    cvs.height = Math.round(h * ratio);

    cvs.area = { w, h };
    ctx.scale(ratio, ratio);
}

onload = () => {
    size_setup();
    addEventListener("resize", size_setup);
}

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

تغییر اندازه‌ی عنصر canvas

وقتی عنصر canvas دچار تغییر اندازه در ابعاد خود می‌شود، یعنی وقتی ویژگی‌های width و height آن تعیین می‌شوند، مثل این است که عنصر canvas تازه‌ای ایجاد شده باشد. یعنی تمام ترسیمات درون آن پاک می‌شوند، تمام ویژگی‌های آن از جمله سایه، خط‌چین، ویژگی‌های fillStyle، strokeStyle و… همگی به مقدار پیش‌فرض خود برمی‌گردند، شکل فعلی آن پاک می‌شود، تبدیلات اعمال شده روی آن حذف می‌شوند، و تمام اطلاعات ذخیره‌شده توسط متد save نیز از بین می‌روند. درست مثل اینکه یک عنصر canvas کاملا تازه جایگزین عنصر قبلی شود!

این اتفاق هر بار که ویژگی‌های width و height عنصر canvas تعیین شوند، پیش می‌آید. یعنی اگر هر یک از کد‌های زیر اجرا شوند، باعث این اتفاق می‌شوند؛ حتی خط آخر که هیچ تغییری در اندازه‌ی عنصر ایجاد نمی‌کند!


cvs.width = 0;
cvs.width = cvs.height;

cvs.width = cvs.width;
cvs.height = cvs.height;

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


#cvs {
    width: 100vw;
    height: 100vh;

    position: absolute;
    top: 0; left: 0;
}

برنامه را با کد بالا اجرا کرده و بزرگنمایی را روی ترسیمات آزمایش کنید. همانطور که می‌بینید نه‌تنها اندازه‌ی ترسیمات ثابت می‌مانند، بلکه هیچ مشکلی در کیفیت ترسیمات جدید ایجاد نمی‌شود. در برنامه‌هایی که اندازه‌ی عنصر با واحد‌های پویایی مانند vw و vh تعیین شده‌اند، چنین مشکلی پیش نمی‌آید، اما همانطور که می‌دانید موارد بسیاری هستند که نمی‌توان از این واحد‌ها استفاده کرد.

البته، مورد بالا هنوز در دو مورد اشکال دارد. مورد اول این است که مکان نشانگر، هنگام کلیک، با مکان ترسیمات همخوانی ندارد. برای حل این مشکل راه‌حل‌های ساده‌ای وجود دارد اما آن را به مخاطب می‌سپاریم! (راهنمایی: باید مقدار اولیه‌ی ویژگی devicePixelRatio ذخیره شود و هنگام دریافت مختصات نشانگر موس، از نسبت مقدار قبلی و مقدار فعلی به عنوان ضریب استفاده شود.) مشکل دوم کمی پیچیده‌تر است و در ادامه به آن اشاره می‌کنیم.

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

  • برای شکل‌ها از کلاس Path2D استفاده کنید.
  • برای بازگردانی ترسیمات پاک‌شده می‌توانید از کلاس ImageData استفاده کنید، اما این مورد در بسیاری از برنامه‌ها پیشنهاد نمی‌شود.
  • اگر درون انیمیشن، ویژگی‌هایی مانند fillStyle دائما تغییر می‌کنند، نگرانی‌ای از بابت آن‌ها نخواهد بود.
  • اگر برخی ویژگی‌ها فقط یک بار تعیین می‌شوند، باید پس از تغییر اندازه دوباره تعیین شوند.
  • نسبت به برنامه‌ی نوشته‌شده، باید اقدامات مناسبی صورت بگیرد.
  • اگر می‌خواهید برخی اندازه‌ها ثابت باشند، آن‌ها را به صورت درصدی، یعنی نسبت به اندازه‌ی عنصر canvas تعیین کنید.

مشکل اینجاست که یک راه‌حل جامع برای حل این مشکل وجود ندارد، اما خبر خوب اینکه بیشتر برنامه‌های canvas بنا به طبیعت خود و کاری که انجام می‌دهند، نیاز به اقدامات خاصی ندارند و می‌توان حتی بدون ایجاد هیچ تغییری روند برنامه را ادامه داد. خلاصه اگر یک برنامه به خوبی نوشته شده باشد، این تغییرات تاثیر خاصی روی آن نخواهند داشت.

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

استفاده از resetTransform

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

بهترین راه‌حل برای این مورد استفاده از متد‌های save و restore است، هرچند مواردی هستند که در آن‌ها استفاده از این متد‌ها ممکن نیست، اما در بیشتر موارد می‌توان از این متد‌ها استفاده کرد و مشکلی وجود ندارد.

ترسیمات در صفحات واکنش‌گرا (responsive)

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


#cvs {
    width: 100vw;
    height: 100vh;

    position: absolute;
    top: 0; left: 0;
}

یک مورد دردسرساز این است که اندازه‌ی ترسیمات، شبیه به اندازه‌ی دیگر عناصر سند، با بزرگنمایی تغییر اندازه می‌دهند. کد زیر را در نظر بگیرید. در این کد که مشابه کد‌های بالاست، با بزرگنمایی صفحه، اندازه‌ی ترسیمات درون عنصر نیز بزرگ می‌شوند. کد زیر را همراه با کد‌های CSS بالا اجرا کنید:


let cvs = document.getElementById("cvs"),
    ctx = cvs.getContext("2d"),
    
    random = (min, max) => ~~(Math.random() * (max - min)) + min;

function size_setup () {
    let w = cvs.clientWidth,
        h = cvs.clientHeight,
        r = devicePixelRatio || 1;
    
    cvs.width = Math.round(w * r);
    cvs.height = Math.round(h * r);

    ctx.scale(r, r);
    cvs.area = { w, h };
}

let main_arc = new Path2D;
main_arc.arc(0, 0, 50, 0, Math.PI * 2);

function draw_arcs () {
    for (let i = 0; i < 100; i++) {
        ctx.save();
        
        ctx.translate(random(0, cvs.area.w), random(0, cvs.area.h));
        ctx.fillStyle = `hsl(${random(0, 360)}, 100%, 50%)`;
        
        ctx.fill(main_arc);
        ctx.restore();
    }
}

onload = () => {
    addEventListener("resize", () => {
        size_setup();
        initialize_canvas();

        draw_arcs();
    });

    size_setup();
    initialize_canvas();

    draw_arcs();
}

function initialize_canvas () {
    ctx.lineWidth = 3;
    ctx.lineCap = ctx.lineJoin = "round";
}

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

راه‌حل این مشکل چیست؟ یک راه‌حل این است که اندازه‌ی عنصر را نسبت به اندازه‌ی صفحه‌ی نمایشگر تعیین کرده و هنگام بزرگنمایی، از تعیین‌اندازه‌ی مجدد خودداری کنیم، اما این مورد هنگام اجرا در رایانه‌های رومیزی (desktop) یک اشکال بزرگ دارد. کافیست اندازه‌ی کل مرورگر را به صورت دستی تغییر دهید!

راه‌حل بهتر چیست؟ بهترین کار این است که هنگام تغییر‌اندازه‌ی صفحه (بزرگنمایی یا تغییر اندازه) اندازه‌ی عنصر را دوباره تعیین کنیم. برای ثابت نگه داشتن اندازه‌ی ترسیمات می‌توان از اندازه‌ی کل صفحه یا عنصر استفاده کرد. شبیه به واحد‌های vw و vh، می‌توانیم یک درصد از اندازه‌ی صفحه‌نمایش (viewport) را ذخیره کرده و از آن به عنوان ضریب در ترسیمات استفاده کنیم.

برای نمونه به کد زیر دقت کنید. کد بالا را به صورت زیر اصلاح کرده و آن را اجرا کنید. با بزرگنمایی صفحه خواهید دید که اندازه‌ی ترسیمات، با وجود اجرای تابع size_setup هنگام تغییر اندازه، ثابت می‌ماند و تغییری نمی‌کند:


let cvs = document.getElementById("cvs"),
    ctx = cvs.getContext("2d"),
    
    VW, VH,
    main_arc,

    random = (min, max) => ~~(Math.random() * (max - min)) + min;

function size_setup () {
    let w = cvs.clientWidth,
        h = cvs.clientHeight,
        r = devicePixelRatio || 1;
    
    cvs.width = Math.round(w * r);
    cvs.height = Math.round(h * r);

    ctx.scale(r, r);
    cvs.area = { w, h };

    VW = w / 100; /* innerWidth / 100 */
    VH = h / 100; /* innerHeight / 100 */
}

function draw_arcs () {
    for (let i = 0; i < 100; i++) {
        ctx.save();
        
        ctx.translate(random(0, cvs.area.w), random(0, cvs.area.h));
        ctx.fillStyle = `hsl(${random(0, 360)}, 100%, 50%)`;
        
        ctx.fill(main_arc);
        ctx.restore();
    }
}

onload = () => {
    addEventListener("resize", () => {
        size_setup();
        initialize_canvas();

        draw_arcs();
    });

    size_setup();
    initialize_canvas();

    draw_arcs();
}

function initialize_canvas () {
    ctx.lineWidth = 3;
    ctx.lineCap = ctx.lineJoin = "round";

    main_arc = new Path2D;
    main_arc.arc(0, 0, 5 * VW, 0, Math.PI * 2);
}

روند کار برای دیگر حالت‌های واکنشگرایی نیز مانند کد بالاست، یعنی نیازی نیست برای هر اندازه‌ی صفحه، یک تابع جداگانه نوشته شود.

نتیجه‌گیری

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

این آموزش، آخرین قسمت از دوره‌ی آموزش canvas است. در طول این آموزش‌ها سعی بر آن بود که به ریزترین جزئیات هر مورد نیز پرداخته شود، اما مواردی نیز از این آموزش حذف شدند؛ از جمله برخی ویژگی‌ها مانند filter و ماتریس تبدیلات. علت این کار خودداری از پیچیدگی بیش‌از حد و همچنین سطح دانش شخص بنده و مخاطبان عزیز است.

احتمالا در دنباله‌ی این آموزش نیز مقالاتی خواهد بود ولی دیگر نمی‌توان نام «آموزش» روی آن‌ها گذاشت زیرا در آن‌ها بیشتر به پروژه‌های بزرگ خواهیم پرداخت. اگر فرصتی باشد، به موارد حذف‌شده در این دوره نیز خواهیم پرداخت، اما این مورد قطعی نیست.

حسین رفیعی

حسین رفیعی

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

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

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