canvas composing

تعامل شکل فعلی و لایه‌ی ترسیمات در canvas

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

ویژگی globalAlpha

این ویژگی یک شفافیت کلی برای تمام ترسیمات تعیین می‌کند. مقدار پیش‌فرض آن 1 (بدون شفافیت) است و همیشه عددی بین 0 و 1 می‌پذیرد. اگر عددی خارج از این بازه به آن بدهیم، مقدار آن به 1 برمی‌گردد. ممکن است با خود بگویید «با وجود فرمت‌های رنگ مانند rgba که درون خود شفافیت دارند، چه نیازی به این ویژگی است؟» پاسخ این است که مواردی مانند تصویر وجود دارند که نمی‌توان برای آن‌ها به کمک rgba شفافیت ایجاد کرد. در این موارد این ویژگی کاربرد دارد. به نمونه کد زیر دقت کنید:


ctx.fillStyle = "#111";
ctx.fillRect(0, 0, cvs.width, cvs.height);

ctx.globalAlpha = 0.5;

ctx.arc(cvs.width / 2, cvs.height / 2, 300, 0, Math.PI * 2);
ctx.fillStyle = "#3D3";
ctx.fill();

ctx.fillStyle = "#0AE";
ctx.fillRect(50, 50, 200, 300);

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

ویژگی globalCompositeOperation

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


ctx.globalCompositeOperation = "source-over" || "source-in" || "source-out" ||
                               "source-atop" || "destination-over" || "destination-in" ||
                               "destination-out" || "destination-atop" || "lighter" ||
                               "copy" || "xor" || "multiply" || "screen" || "overlay" ||
                               "darken" || "lighten" || "color-dodge" || "color-burn" ||
                               "hard-light" || "soft-light" || "difference" || "exclusion" ||
                               "hue" || "saturation" || "color" || "luminosity";

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

  • source-over ترسیمات جدید روی ترسیمات قبلی رسم می‌شوند. (مقدار پیش‌فرض)
  • source-in بخش‌هایی از ترسیمات جدید باقی می‌مانند که روی ترسیمات قبلی باشند.
  • source-out بخش‌هایی از ترسیمات جدید باقی می‌مانند که روی ترسیمات قبلی نباشند.
  • source-atop شبیه به source-in ولی ترسیمات دیگر حذف نمی‌شوند.
  • destination-over ترسیمات قبلی روی ترسیمات جدید قرار می‌گیرند.
  • destination-in بخش‌هایی از ترسیمات قبلی باقی می‌مانند که روی ترسیمات جدید قرار می‌گیرند.
  • destination-out بخش‌هایی از ترسیمات قبلی باقی می‌مانند که روی ترسیمات جدید قرار نمی‌گیرند.
  • destination-atop فقط بخش‌هایی از ترسیمات قبلی باقی می‌مانند که روی ترسیمات جدید قرار می‌گیرند؛ و ترسیمات جدید نیز پشت آن‌ها رسم می‌شوند.
  • copy فقط ترسیمات جدید باقی می‌مانند. دیگر ترسیمات حذف می‌شوند.
  • xor بخش‌هایی که ترسیمات قبلی و جدید روی هم قرار می‌گیرند حذف می‌شوند. دیگر بخش‌ها به صورت عادی رسم می‌شوند.
  • lighter در بخش‌هایی که ترسیمات قبلی و جدید روی هم قرار می‌گیرند، مقدار‌های رنگ با هم جمع می‌شوند.
  • multiply هر پیکسل از ترسیمات جدید با پیکسل ترسیمات قبلی ضرب می‌شود. نتیجه رنگ‌های تیره‌تر است.
  • screen برعکس مقدار multiply نتیجه رنگ‌های روشن‌تر است.
  • overlay ترکیبی از multiply و screen که در آن بخش‌های تیره، تیره‌تر و بخش‌های روشن، روشن‌تر می‌شوند.
  • darken تیره‌ترین رنگ‌ها از هرکدام از ترسیمات باقی می‌مانند.
  • lighten روشن‌ترین رنگ‌ها از هرکدام از ترسیمات باقی می‌مانند.
  • color-dodge رنگ زیرین را به معکوس رنگ بالا تقسیم می‌کند.
  • color-burn معکوس رنگ زیرین را به رنگ بالا تقسیم می‌کند.
  • hard-light شبیه به overlay اما جای ترسیمات جدید و قبلی عوض می‌شود.
  • soft-light نسخه‌ی ملایم‌تری از hard-light که رنگ‌ها در آن بیشتر ترکیب می‌شوند.
  • difference تفاوت لایه‌ی زیرین را از لایه‌ی بالا استفاده می‌کند. اگر مقدار منفی شود آن را قرینه می‌کند.
  • exclusion شبیه به difference اما با contrast کمتر
  • hue نتیجه‌ی این مقدار یک رنگ HSL است که saturation و luma آن از رنگ قبلی، و hue آن از رنگ جدید است.
  • saturation شبیه به hue اما با این مقدار، saturation از ترسیمات جدید استفاده می‌شود.
  • luminosity شبیه به hue و saturation اما با این مقدار، luma یا رنگ از ترسیمات جدید استفاده می‌شود.
  • color برعکس luminosity از رنگ ترسیمات قبلی استفاده شده ولی دیگر مقادیر از ترسیمات جدید استفاده می‌شوند.
  • darker این مقدار در نسخه‌های قدیمی مرورگر‌ها وجود داشت اما به دلایلی دیگر پشتیبانی نمی‌شود.

همانطور که می‌بینید، تعداد این مقدایر زیاد بوده و رفتار هرکدام نیز با دیگر مقادیر تفاوت زیادی دارد. از بین این 26 مقدار، چهار مقدار مربوط به source، چهار مقدار مربوط به destination، و دو مقدار xor و copy هستند که به ساختار کلی ترسیمات اثر می‌گذارند. دیگر مقادیر همگی در بازه‌ی رنگ‌ها هستند. این موارد می‌توانند در ایجاد افکت‌های تصویری بسیار کاربردی باشند اما به اندازه‌ی مقدار‌های قبلی در همه‌جا کاربرد ندارند.

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

canvas main compositions

حال به مقدار‌های مربوط به رنگ می‌پردازیم. برای درک بهتر رفتار آن‌ها باید از ترسیماتی استفاده کنیم که دارای طیف رنگ و شفافیت هستند. کد زیر یک تصویر مناسب برای این کار ایجاد می‌کند. در کد زیر به جای مقدار source-over مقدار‌های متفاوت قرار داده و نتیجه را ببینید. همچنین می‌توانید از تصویر پایین کمک بگیرید:


cvs.width = cvs.height = 500;

let g1 = ctx.createRadialGradient(300, 240, 0, 260, 300, 460),
    g2 = ctx.createRadialGradient(160, 180, 0, 120, 160, 460);

g1.addColorStop(0.20, "#FC0");
g1.addColorStop(0.05, "#3D0");
g1.addColorStop(0.70, "transparent");

g2.addColorStop(0.05, "#3D0");
g2.addColorStop(0.20, "#3BD");
g2.addColorStop(0.70, "transparent");

ctx.fillStyle = g1;
ctx.arc(260, 300, 460, 0, Math.PI * 2);
ctx.fill();

ctx.beginPath();
ctx.globalCompositeOperation = "source-over";

ctx.fillStyle = g2;
ctx.arc(120, 160, 460, 0, Math.PI * 2);
ctx.fill();

canvas color compositions

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

پاک کردن لایه‌ی ترسیمات به شکل دلخواه

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


ctx.fillStyle = "#3D3";
ctx.fillRect(50, 50, 300, 200);

ctx.globalCompositeOperation = "destination-out";

ctx.arc(160, 200, 130, 0, Math.PI * 2);
ctx.fill();

محدود کردن لایه‌ی ترسیمات بدون نیاز به شکل فعلی

گاهی پیش می‌آید که می‌خواهید لایه‌ی ترسیمات را محدود کنید اما بنا به دلایلی نمی‌توانید شکل فعلی را به درستی تعیین کرده یا آن را تغییر دهید. در این موقعیت می‌توانید از مقدار source-atop استفاده کنید. با این مقدار می‌توانید بدون نیاز به تعیین شکل فعلی و استفاده از متد clip، ترسیمات بعدی را به محدوده‌ی ترسیمات فعلی درون لایه‌ی ترسیمات محدود کنید. این ویژگی را برای کد بالا امتحان کرده و نتیجه را ببینید. بخش‌هایی از دایره که خارج از محدوده‌ی ترسیمات قبلی (همان مستطیل) باشند، رسم نمی‌شوند.

پاک کردن لایه‌ی ترسیمات بدون دردسر

در canvas روش‌های زیادی برای پاک کردن لایه‌ی ترسیمات وجود دارد، منتها همگی حداقل یک مشکل همراه خود دارند. ساده‌ترین روش این است که از متد clearRect استفاده کنیم. اما این متد از متد‌های transform تاثیر می‌پذیرد. با وجود اینکه می‌توان متد‌های transform را دور زد، اما روش بسیار ساده‌ای برای پاک کردن لایه‌ی ترسیمات وجود دارد و آن استفاده از مقدار copy است! به کد زیر توجه کنید:


function clear (context) {
    let previous_composite = context.globalCompositeOperation,
        previous_color = context.fillStyle;
    
    context.globalCompositeOperation = "copy";
    context.fillStyle = "rgba(0, 0, 0, 0)";
    
    context.fillRect(0, 0, 1, 1);
    
    context.globalCompositeOperation = previous_value;
    context.fillStyle = previous_color;
}

کد بالا یک تابع تعریف می‌کند که زمینه‌ی موردنظر را به عنوان ورودی دریافت می‌کند. سپس مقدار فعلی ویژگی globalCompositeOperation و fillStyle را ذخیره می‌کند؛ سپس آن را به copy تغییر داده و رنگ را نیز کاملا شفاف می‌کند. حال یک مربع به طول و عرض 1 رسم می‌کند. سپس مقدار قبلی ویژگی‌ها را پس می‌دهد. این روش یکی از ساده‌ترین و سریع‌ترین روش‌ها برای پاک کردن ترسیمات canvas است. در آموزش‌های آینده توضیحات بیشتری در این باره آورده شده است.

ایجاد توهم سه‌بعدی

با اینکه در canvas نمی‌توان ترسیمات سه‌بعدی انجام داد، اما اگر مقدار destination-over را با سایه و رنگ درست ترکیب کنید، می‌توانید یک توهم سه‌بعدی زیبا ایجاد کنید. از این ویژگی در ترسیمات پیشرفته در آینده استفاده خواهد شد.

ایجاد جلوه‌های تصویری

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


let img = document.getElementById("an_image");

cvs.width = 1500;
cvs.height = 500;

onload = () => {
    img = (() => {
        let c = document.createElement("canvas");
        c.width = c.height = 500;
        
        c = c.getContext("2d");
        c.drawImage(img, 0, 0, 500, 500);
        
        return c.canvas;
    }) ();
    
    ctx.drawImage(img, 0, 0);
    
    ctx.drawImage(img, 500, 0);
    ctx.globalCompositeOperation = "color";
    
    ctx.fillStyle = "rgba(255, 0, 0, 0.5)";
    ctx.fillRect(500, 0, 500, 500);
    
    ctx.drawImage(img, 1000, 0);
    ctx.globalCompositeOperation = "color";
    
    ctx.fillStyle = "rgb(0, 0, 0)";
    ctx.fillRect(1000, 0, 500, 500);
}

canvas image effect compositions

نکات پایانی

جدا از مواردی که بررسی شد، دو نکته‌ی مهم درباره‌ی این ویژگی وجود دارد که باید همیشه به آن‌ها توجه داشته باشید.

نوع ترسیمات روی نتیجه اثرگذار هستند

این مورد با اینکه تا حدودی بدیهی است، اما بسیاری آن را فراموش می‌کنند. گفتیم ویژگی globalCompositeOperation نوع تعامل ترسیمات جدید را با ترسیمات پیشین تعیین می‌کند. این یعنی اگر شکل با متد stroke رسم شود، نتیجه‌ی متفاوتی با متد fill خواهد داشت. حتی الگوی ترسیمات (ورودی متد fill) نیز می‌توانند نتایج متفاوتی داشته باشند! برای درک بهتر موضوع، در کد‌های بالا، به جای متد fill از متد stroke استفاده کنید تا بهتر متوجه شوید.

وجود شفافیت در ترسیمات مهم است

در اینجا متوجه می‌شوید که کاربرد ویژگی globalAlpha آنقدرها هم کم نیست! شفافیتی که در ترسیمات وجود دارد، روی رفتار globalCompositeOperation اثرگذار است. در واقع این ویژگی، بسته به میزان شفافیت ترسیمات روی آن‌ها اثر می‌گذارد. برای نمونه اگر ترسیمات دارای 95% شفافیت باشند، این ویژگی فقط روی 5% غیرشفاف تاثیر می‌گذارد. برای درک بهتر به کد زیر توجه کنید:


ctx.fillStyle = "rgba(30, 200, 30)";
ctx.fillRect(50, 50, 300, 200);

ctx.fillStyle = "rgba(30, 200, 30, 0.1)";
ctx.arc(160, 200, 130, 0, Math.PI * 2);

cvs.addEventListener("click", () => {
    ctx.globalCompositeOperation = "destination-out";
    ctx.fill();
});

در کد بالا که مشابه کد قبل برای پاک کردن ترسیمات است، شکل اولیه رسم شده، سپس یک دایره برای پاک کردن لایه‌ی ترسیمات وارد شکل فعلی می‌شود، نکته‌ی مهم این است که ویژگی fillStyle دارای شفافیت 90% است، این یعنی ویژگی globalCompositeOperation بسته به این شفافیت عمل می‌کند. با هر بار کلیک روی عنصر cvs، متد fill یک بار اجرا می‌شود.

احتمالا انتظار دارید مانند کد قبل، با یک بار کلیک تمام ترسیمات که زیر دایره قرار می‌گیرند پاک شوند، اما بعد از امتحان کردن متوجه این ویژگی خواهید شد! همانطور که می‌بینید، با هر بار کلیک فقط 10% شفافیت ترسیمات کم می‌شود، علت این است که ترسیمات جدید 90% شفافیت داشتند و این ویژگی روی بخش‌های شفاف ترسیمات کار نمی‌کند. (درست شبیه سایه)

ترکیب شفافیت با این ویژگی (چه به واسطه‌ی فرمت رنگ rgba، hsla، یا ویژگی globalAlpha)، کاربرد‌های این ویژگی را باز هم بیشتر می‌کند. استفاده از این ترکیب در انیمیشن‌ها می‌تواند ترسیمات فوق‌العاده‌ای ایجاد کند که بازسازی آن‌ها بدون این ترکیب غیرممکن است!

جدا از تمام مواردی که گفته شد، لازم است اشاره‌ای نیز به دو متد setAlpha و setCompositeOperation داشته باشیم. این دو متد در مرورگر‌های رده webkit پشتیبانی می‌شدند اما سال‌ها پیش منقضی شده‌اند.

نتیجه‌گیری

همانطور که تاکنون متوجه شده‌اید، این دو ویژگی (برخلاف ظاهرشان) بسیار کاربردی هستند و با کوچکترین تغییر، بیشترین تاثیر را بر ترسیمات می‌گذارند. پیشنهاد می‌شود این بخش را به خوبی مطالعه کرده و یاد بگیرید، زیرا این موارد جزء مهم‌ترین ویژگی‌های canvas هستند. کاربرد‌هایی که در این بخش بررسی شد، فقط چند مورد جزئی از کاربرد‌های فراوان این ویژگی (یا ترکیب این دو ویژگی) هستند. در بخش بعدی به تبدیلات (transform) خواهیم پرداخت.

حسین رفیعی

حسین رفیعی

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

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

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