<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>آموزش طراحی وب</title>
	<atom:link href="https://css-tricks.ir/feed/" rel="self" type="application/rss+xml" />
	<link>https://css-tricks.ir/</link>
	<description>آموزش تخصصی SVG &#124; CSS &#124; HTML و دیگر تکنولوژی‌های Front-end</description>
	<lastBuildDate>Wed, 08 Dec 2021 03:45:06 +0000</lastBuildDate>
	<language>fa-IR</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.8.5</generator>
	<item>
		<title>تعیین اندازه canvas و تعامل آن با CSS</title>
		<link>https://css-tricks.ir/tutorial/%d8%aa%d8%b9%db%8c%db%8c%d9%86-%d8%a7%d9%86%d8%af%d8%a7%d8%b2%d9%87-canvas-%d9%88-%d8%aa%d8%b9%d8%a7%d9%85%d9%84-%d8%a2%d9%86-%d8%a8%d8%a7-css/</link>
					<comments>https://css-tricks.ir/tutorial/%d8%aa%d8%b9%db%8c%db%8c%d9%86-%d8%a7%d9%86%d8%af%d8%a7%d8%b2%d9%87-canvas-%d9%88-%d8%aa%d8%b9%d8%a7%d9%85%d9%84-%d8%a2%d9%86-%d8%a8%d8%a7-css/#comments</comments>
		
		<dc:creator><![CDATA[حسین رفیعی]]></dc:creator>
		<pubDate>Wed, 08 Dec 2021 03:43:08 +0000</pubDate>
				<category><![CDATA[آموزش]]></category>
		<category><![CDATA[canvas]]></category>
		<category><![CDATA[css]]></category>
		<category><![CDATA[resize]]></category>
		<guid isPermaLink="false">https://css-tricks.ir/?p=9496</guid>

					<description><![CDATA[<p>در این آموزش به روش‌های تعیین‌اندازه‌ی canvas، تعامل با CSS و مشکلات مربوط به آن در حالت‌های مختلف می‌پردازیم.</p>
<p>نوشته <a rel="nofollow" href="https://css-tricks.ir/tutorial/%d8%aa%d8%b9%db%8c%db%8c%d9%86-%d8%a7%d9%86%d8%af%d8%a7%d8%b2%d9%87-canvas-%d9%88-%d8%aa%d8%b9%d8%a7%d9%85%d9%84-%d8%a2%d9%86-%d8%a8%d8%a7-css/">تعیین اندازه canvas و تعامل آن با CSS</a> اولین بار در <a rel="nofollow" href="https://css-tricks.ir">آموزش طراحی وب</a> پدیدار شد.</p>
]]></description>
										<content:encoded><![CDATA[<p>در این آموزش چیز جدیدی درباره‌ی canvas بررسی نخواهد شد. یعنی در این فصل با متد‌های جدیدی برای ترسیمات آشنا نخواهید شد؛ اما مفاهیمی که در این آموزش بازگو می‌شوند بسیار مهم هستند و تقریبا در هر پروژه‌ای باید رعایت شوند. در کنار این مفاهیم، به مواردی نیز خواهیم پرداخت که به دلایلی به آن‌ها اشاره نکرده‌ایم.</p>
<p>برای سادگی کار و انجام درست آزمایش‌هایمان، برنامه‌ی زیر را در canvas اجرا می‌کنیم. در این برنامه، با هر کلیک کاربر روی عنصر، یک دایره در آن مختصات با رنگ تصادفی رسم می‌شود. شعاع دایره ثابت و برابر 50 است:</p>
<pre><code class="language-javascript">
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);
</code></pre>
<p>در یکی از آموزش‌های اولیه گفتیم که تعیین ویژگی‌های <code>width</code> و <code>height</code> در CSS با تعیین این ویژگی‌ها برای خود عنصر متفاوت است. برای درک بهتر این تفاوت، باید به عنصر canvas به چشم یک تصویر نگاه کنید. تصویری که ابعاد آن را می‌توانیم تعیین کنیم و محتوای درون آن را نیز تغییر دهیم.</p>
<p>وقتی ویژگی‌های <code>width</code> و <code>height</code> یک عنصر canvas را تعیین می‌کنیم، یک تصویر با این اندازه‌ها ایجاد می‌کنیم؛ و وقتی با CSS این ویژگی‌ها را تعیین می‌کنیم، این تصویر به ابعاد موردنظر تغییر اندازه (scale) می‌دهد. برای درک بهتر موضوع کد زیر را اجرا کنید. در این کد اندازه‌ی اصلی عنصر <code>cvs</code> به 700 ولی ابعاد آن در CSS متفاوت تعیین شده‌اند:</p>
<pre><code class="language-javascript">
let cvs = document.getElementById("cvs"),
    ctx = cvs.getContext("2d");

cvs.width = 700;
cvs.height = 700;
</code></pre>
<pre><code class="language-css">
#cvs {
    width: 600px;
    height: 200px;
}
</code></pre>
<p>کد را اجرا کرده و روی عنصر کلیک کنید. همانطور که می‌بینید، به دلیل این تغییر اندازه‌ی ناخواسته، رفتار برنامه کاملا خراب شده است. اگر به تعیین اندازه‌ی canvas به صورت دستی عادت کرده باشید، این مشکل بزرگی برای شما به حساب می‌آید، زیرا در بسیاری از پروژه‌ها چنین تغییر اندازه‌هایی کاملا عادی است.</p>
<h2>تعیین اندازه‌ی عنصر canvas</h2>
<p>تا به حال در تمام برنامه‌هایی که بررسی کرده‌ایم، اندازه‌ی عنصر را به صورت دستی تعیین کرده‌ایم، ولی در این آموزش باید ببینیم چطور می‌توان تغییر انداز‌ه‌های ناخواسته را خنثی کرد تا رفتار برنامه دچار اختلال نشود. بهترین راه‌حل، این است که ابتدا اندازه‌ی عنصر را در CSS تعیین کنیم، سپس اندازه‌ی اصلی آن را نسبت به همان اندازه تغییر دهیم. برای این کار روش‌های مختلفی وجود دارد، اما مورد مهمی که باید همیشه رعایت کنیم، این است که ابتدا ویژگی‌های CSS روی شکل اعمال شده باشند، سپس اقدام به دریافت اندازه‌ها کنیم. برای سادگی کار می‌توانید ویژگی‌های زیر را به کد خود اضافه کنید. می‌توانید ابعاد عنصر را در کد زیر به دلخواه تغییر دهید. باقی کد برای وسط قرار دادن عنصر است:</p>
<pre><code class="language-css">
#cvs {
    width: 60em;
    height: 40em;
    border: 0.2em solid #111;
    
    position: absolute;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
}
</code></pre>
<h3>استفاده از clientWIdth و clientHeight</h3>
<p>یکی از آسان‌ترین راه‌ها برای دریافت اندازه‌های تعیین‌شده برای عنصر، استفاده از ویژگی‌های <code>clientWidth</code> و <code>clientHeight</code> است. این ویژگی‌ها روی هر عنصری در سند HTML وجود دارند. این اندازه‌ها همیشه عدد صحیح هستند. به کد زیر دقت کنید. در کد زیر به کمک این ویژگی‌ها اندازه‌ی عنصر <code>cvs</code> را تعیین می‌کنیم:</p>
<pre><code class="language-javascript">
function size_setup () {
    let w = cvs.clientWidth,
        h = cvs.clientHeight;
    
    cvs.width = w;
    cvs.height = h;
}

onload = size_setup;
</code></pre>
<p>کد را اجرا کرده و رفتار برنامه را بررسی کنید. همانطور که در خط آخر کد می‌بینید، این تابع باید پس از بارگیری کامل صفحه اجرا شود، زیرا همانطور که گفتیم، ویژگی‌های CSS باید به عنصر اعمال شده باشند.</p>
<h3>استفاده از ClientRect</h3>
<p>روش دیگر استفاده از متد <code>getBoundingClientRect</code> است. پیش از این استفاده از این متد را دیده‌اید. این روش کمی دقیق‌تر است، زیرا مقدار‌های خروجی از این متد عدد صحیح نیستند و می‌توانند بخش اعشاری نیز داشته باشند. به کد زیر دقت کنید. در کد زیر به کمک این متد اندازه‌ی عنصر <code>cvs</code> به دست می‌آید:</p>
<pre><code class="language-javascript">
function size_setup () {
    let { width, height } = cvs.getBoundingClientRect();
    
    cvs.width = Math.round(width);
    cvs.height = Math.round(height);
}

onload = size_setup;
</code></pre>
<p>علت اینکه در کد بالا از <code>Math.round</code> استفاده شد، این است که اندازه‌های canvas باید اعداد صحیح باشند. می‌توانید به جای تابع <code>round</code> از توابع <code>ceil</code> و <code>floor</code> و&#8230; نیز استفاده کنید.</p>
<p>به جز این دو روش، روش‌های دقیق‌تر دیگری نیز مانند <code>Resize Observer</code> وجود دارند، اما در مسائل مربوط به فناوری canvas دو‌بعدی، استفاده از یکی از دو روش بالا کفایت می‌کند. روش‌های دقیق‌تر برای موارد مهم‌تر مانند WebGL کاربرد دارند.  خب همانطور که می‌بینید، مشکل تعیین اندازه در canvas حل شد! اما مشکل بزرگ‌تری هست که حل کردن آن دردسر بیشتری دارد.</p>
<h2>افت کیفیت canvas</h2>
<p>یکی از مشکلات اصلی در canvas، افت کیفیت ترسیمات آن در نمایشگر‌های با کیفیت بالا است. علت این مشکل، تعریف واحد پیکسل در CSS است. به دلایل مختلف، از جمله انعطاف‌پذیری بیشتر صفحات وب، اندازه‌ی هر پیکسل CSS با هر پیکسل نمایشگر تفاوت دارد. این تفاوت در مواردی مانند ترسیمات برداری که شامل CSS و SVG می‌شود، مشکلی ایجاد نمی‌کند، اما وقتی پای ترسیمات شطرنجی به میدان بیاید، و از پیکسل‌های CSS برای تعیین اندازه‌ی عناصر canvas استفاده کنیم، دچار افت کیفیت می‌شویم.</p>
<p>استفاده از <code>Resize Observer</code> می‌تواند این مشکل را حل کند، اما این فناوری پیچیدگی‌های خود را دارد و همچنین پشتیبانی مرورگر‌ها نیز از آن نسبتا پایین است و باید به دنبال یک راه‌حل ساده‌تر و با پشتیبانی بالاتر باشیم.</p>
<h3>ویژگی devicePixelRatio</h3>
<p>این ویژگی نسبت اندازه‌ی پیکسل CSS را به پیکسل نمایشگر به ما می‌دهد. به کمک این ویژگی می‌توانیم از افت کیفیت ترسیمات جلوگیری کنیم. به کد زیر دقت کنید. در کد زیر از این ویژگی برای تعیین اندازه‌ی درست عنصر استفاده شده است:</p>
<pre><code class="language-javascript">
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);
}
</code></pre>
<p>هنگام استفاده از این ویژگی نیز باید از تابع <code>round</code> یا توابع دیگر استفاده کنیم، زیرا این مقدار معمولا عدد صحیح نیست. کد را اجرا کرده و نتیجه را ببینید. اگر نمایشگر شما معمولی باشد، تغییری مشاهده نخواهید کرد، پس می‌توانید به صورت دستی مقدار <code>ratio</code> را به 2 یا بیشتر تغییر دهید. حال کد را اجرا کرده و نتیجه را ببینید.</p>
<p>کد بالا توانست مشکل کیفیت را حل کند، اما دو مشکل دیگر ایجاد کرد، و اکنون باید به دنبال راه‌حل این مشکلات باشیم! مشکل اول اینکه اندازه‌ی ترسیمات به نسبت این ویژگی کوچک‌تر شده‌اند. این یعنی در دو نمایشگر با کیفیت متفاوت، اندازه‌ی ترسیمات نیز متفاوت خواهند بود! برای حل این مشکل لازم است ترسیمات را به اندازه‌ی <code>devicePixelRatio</code> تغییر اندازه بدهیم. کد بالا را به صورت زیر اصلاح کرده و نتیجه را ببینید:</p>
<pre><code class="language-javascript">
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);
}
</code></pre>
<p>با اجرای کد می‌بینید که مشکل دوم نیز حل شد! مشکل دوم برنامه این بود که مختصات نشانگر با مختصات ترسیمات همخوانی نداشت اما با این تغییر اندازه، این مشکل نیز حل شد. اما مشکل سومی نیز وجود دارد! اینکه در این وضعیت، ابعاد لایه‌ی ترسیمات با ابعاد عنصر canvas متفاوت است.</p>
<p>در این حالت جدید، ترسیمات canvas در واحد پیکسل CSS رسم می‌شوند، ولی اندازه‌ی عنصر دیگر در این واحد نیست. برای درک بهتر این مشکل، کد زیر را اجرا کنید. در کد زیر، تعداد 100 دایره در مختصات تصادفی درون canvas رسم می‌شوند:</p>
<pre><code class="language-javascript">
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 &lt; 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 = () =&gt; {
    size_setup();
    draw_arcs();
}
</code></pre>
<p>با دقت به نتیجه‌ی این کد متوجه می‌شوید که تعدادی از دایره‌ها، خارج از لایه‌ی ترسیمات رسم شده‌اند و دیده نمی‌شوند. مشکل از کجاست؟ مشکل این است که اندازه‌ی محدوده‌ی فعلی ترسیمات با ابعاد عنصر canvas همخوانی ندارد. در کد بالا از ابعاد خود عنصر canvas درون تابع <code>random</code> استفاده کرده‌ایم، در حالی که این ابعاد از محدوده‌ی فعلی ترسیمات فراتر می‌روند.</p>
<p>این موضوع شاید در برنامه‌ی بالا مشکل خاصی به حساب نیاید، ولی در برخی برنامه‌ها می‌تواند رفتار برنامه را به‌کلی خراب کند و مشکلات زیادی ایجاد کند. راه‌حل این مشکل چیست؟ می‌توانیم ابعاد محدوده‌ی ترسیمات عنصر را جداگانه ذخیره کرده و هنگام نیاز استفاده کنیم. به کد زیر دقت کنید. در کد زیر متغیر <code>area</code> تعریف شده و اندازه‌ی محدوده‌ی ترسیمات عنصر <code>cvs</code> را ذخیره می‌کند:</p>
<pre><code class="language-javascript">
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);
}
</code></pre>
<p>طبق تعریفات بالا، از این به بعد متغیر <code>area</code> اندازه‌های اصلی عنصر <code>cvs</code> را درون خود دارد و باید از این اندازه‌ها برای ترسیمات استفاده کنیم. با اصلاح کد رسم دایره‌ها به شکل زیر خواهید دید که مشکل ما نیز حل خواهد شد و تمام دایره‌ها در محدوده‌ی ترسیمات رسم خواهند شد:</p>
<pre><code class="language-javascript">
function draw_arcs () {
    for (i = 0; i &lt; 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();
    }
}
</code></pre>
<p>احتمالا در پروژه‌های بزرگ با چندین عنصر canvas سر و کار خواهید داشت، بنابراین بهتر است به جای استفاده از یک متغیر جدا، ویژگی <code>area</code> را برای هر عنصر canvas تعریف کرده و اندازه‌ها را در آن ذخیره کنیم. به این ترتیب دسترسی به اندازه‌ی هر عنصر ساده‌تر بوده و از شلوغ شدن برنامه نیز جلوگیری می‌شود:</p>
<pre><code class="language-javascript">
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);
}
</code></pre>
<p>خب ظاهرا دیگر مشکلی وجود ندارد، اما اشتباه می‌کنید! در یک صفحه وب معمولا کاربران صفحه را بزرگنمایی می‌کنند، درضمن در برخی برنامه‌های canvas باید امکان تغییر اندازه‌ی عنصر نیز وجود داشته باشد. شاید در ابتدا مشکلی در این مورد وجود نداشته باشد، اما کافیست ویژگی‌های CSS زیر را به عنصر اعمال کرده و پس از اجرای برنامه، صفحه را بزرگنمایی کنید:</p>
<pre><code class="language-css">
#cvs {
    width: 60em;
    height: 40em;
    border: 0.2em solid #111;

    position: absolute;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
}
</code></pre>
<p>در ابتدای اجرای برنامه، مشکلی وجود ندارد، ولی پس از بزرگنمایی، نه‌تنها رفتار برنامه خراب می‌شود، بلکه کیفیت ترسیمات نیز پایین می‌آید. علت این مشکل چیست؟ باز هم تعریف واحد پیکسل CSS! با بزرگنمایی صفحه، اندازه‌ی هر پیکسل CSS نیز تغییر می‌کند و به طبع آن، ویژگی <code>devicePiexlRatio</code> نیز تغییر می‌کند. در وضعیت فعلی، با بزرگنمایی صفحه، اندازه‌ی عنصر ثابت باقی می‌ماند و به همین دلیل کیفیت ترسیمات دچار افت می‌شود.</p>
<p>برای حل این مشکل باید با هر بار تغییر اندازه‌ی صفحه، که بزرگنمایی هم شامل آن می شود، اندازه‌ی عنصر canvas را نیز مثل قبل تعیین کنیم. یعنی کد اصلی را باید به این شکل اصلاح کنیم:</p>
<pre><code class="language-javascript">
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 = () =&gt; {
    size_setup();
    addEventListener("resize", size_setup);
}
</code></pre>
<p>خب ظاهرا به آخرین مشکل کار رسیده‌ایم، و آن تبعات تغییر اندازه‌ی عنصر canvas است. برنامه‌ی رسم دایره‌ها را همراه با کد بالا اجرا کرده و صفحه را بزرگنمایی کنید. همانطور که می‌بینید تمام ترسیمات درون عنصر پاک می‌شوند، ولی این تمام اتفاقی نیست که می‌افتد.</p>
<h2>تغییر اندازه‌ی عنصر canvas</h2>
<p>وقتی عنصر canvas دچار تغییر اندازه در ابعاد خود می‌شود، یعنی وقتی ویژگی‌های <code>width</code> و <code>height</code> آن تعیین می‌شوند، مثل این است که عنصر canvas تازه‌ای ایجاد شده باشد. یعنی تمام ترسیمات درون آن پاک می‌شوند، تمام ویژگی‌های آن از جمله سایه، خط‌چین، ویژگی‌های <code>fillStyle</code>، <code>strokeStyle</code> و&#8230; همگی به مقدار پیش‌فرض خود برمی‌گردند، شکل فعلی آن پاک می‌شود، تبدیلات اعمال شده روی آن حذف می‌شوند، و تمام اطلاعات ذخیره‌شده توسط متد <code>save</code> نیز از بین می‌روند. درست مثل اینکه یک عنصر canvas کاملا تازه جایگزین عنصر قبلی شود!</p>
<p>این اتفاق هر بار که ویژگی‌های <code>width</code> و <code>height</code> عنصر canvas تعیین شوند، پیش می‌آید. یعنی اگر هر یک از کد‌های زیر اجرا شوند، باعث این اتفاق می‌شوند؛ حتی خط آخر که هیچ تغییری در اندازه‌ی عنصر ایجاد نمی‌کند!</p>
<pre><code class="language-javascript">
cvs.width = 0;
cvs.width = cvs.height;

cvs.width = cvs.width;
cvs.height = cvs.height;
</code></pre>
<p>پیش از ادامه بهتر است به یک استثنا بپردازیم. در برنامه‌هایی که بزرگنمایی روی عنصر canvas تاثیر ندارد، با بزرگنمایی کیفیت ترسیمات دچار افت نمی‌شوند و می‌توان به سادگی از تغییر اندازه‌ی عنصر هنگام بزرگنمایی خودداری کرد. برای نمونه کد زیر را اجرا کنید. در کد زیر ابعاد عنصر canvas همواره کل صفحه را در بر می‌گیرند و حتی هنگام بزرگنمایی نیز تغییری در کیفیت ترسیمات ایجاد نمی‌شود:</p>
<pre><code class="language-css">
#cvs {
    width: 100vw;
    height: 100vh;

    position: absolute;
    top: 0; left: 0;
}
</code></pre>
<p>برنامه را با کد بالا اجرا کرده و بزرگنمایی را روی ترسیمات آزمایش کنید. همانطور که می‌بینید نه‌تنها اندازه‌ی ترسیمات ثابت می‌مانند، بلکه هیچ مشکلی در کیفیت ترسیمات جدید ایجاد نمی‌شود. در برنامه‌هایی که اندازه‌ی عنصر با واحد‌های پویایی مانند <code>vw</code> و <code>vh</code> تعیین شده‌اند، چنین مشکلی پیش نمی‌آید، اما همانطور که می‌دانید موارد بسیاری هستند که نمی‌توان از این واحد‌ها استفاده کرد.</p>
<p>البته، مورد بالا هنوز در دو مورد اشکال دارد. مورد اول این است که مکان نشانگر، هنگام کلیک، با مکان ترسیمات همخوانی ندارد. برای حل این مشکل راه‌حل‌های ساده‌ای وجود دارد اما آن را به مخاطب می‌سپاریم! (راهنمایی: باید مقدار اولیه‌ی ویژگی devicePixelRatio ذخیره شود و هنگام دریافت مختصات نشانگر موس، از نسبت مقدار قبلی و مقدار فعلی به عنوان ضریب استفاده شود.) مشکل دوم کمی پیچیده‌تر است و در ادامه به آن اشاره می‌کنیم.</p>
<p>همانطور که می‌بینید، تبعات تغییر اندازه‌ی عنصر سنگین است و راه‌حل خاصی نیز برای آن وجود ندارد. اما می‌توان برای حل مشکلات ناشی از آن اقداماتی انجام داد تا روند برنامه دچار مشکل نشود. البته این اقدامات تماما به برنامه‌ی ما و فرآیند‌های درون آن بستگی دارد، اما فعلا به چند مورد اشاره می‌کنیم:</p>
<ul>
<li>برای شکل‌ها از کلاس <code>Path2D</code> استفاده کنید.</li>
<li>برای بازگردانی ترسیمات پاک‌شده می‌توانید از کلاس <code>ImageData</code> استفاده کنید، اما این مورد در بسیاری از برنامه‌ها پیشنهاد نمی‌شود.</li>
<li>اگر درون انیمیشن، ویژگی‌هایی مانند <code>fillStyle</code> دائما تغییر می‌کنند، نگرانی‌ای از بابت آن‌ها نخواهد بود.</li>
<li>اگر برخی ویژگی‌ها فقط یک بار تعیین می‌شوند، باید پس از تغییر اندازه دوباره تعیین شوند.</li>
<li>نسبت به برنامه‌ی نوشته‌شده، باید اقدامات مناسبی صورت بگیرد.</li>
<li>اگر می‌خواهید برخی اندازه‌ها ثابت باشند، آن‌ها را به صورت درصدی، یعنی نسبت به اندازه‌ی عنصر canvas تعیین کنید.</li>
</ul>
<p>مشکل اینجاست که یک راه‌حل جامع برای حل این مشکل وجود ندارد، اما خبر خوب اینکه بیشتر برنامه‌های canvas بنا به طبیعت خود و کاری که انجام می‌دهند، نیاز به اقدامات خاصی ندارند و می‌توان حتی بدون ایجاد هیچ تغییری روند برنامه را ادامه داد. خلاصه اگر یک برنامه به خوبی نوشته شده باشد، این تغییرات تاثیر خاصی روی آن نخواهند داشت.</p>
<p>توصیه‌ی آخر اینکه برای برنامه‌هایی که می‌نویسید، همیشه تغییرات اندازه را درنظر داشته باشید و اقدامات لازم را برای خنثی کردن آن‌ها انجام دهید. حتی در برنامه‌های بسیار پیچیده نیز مشکل خاصی از این جهت ایجاد نخواهد شد پس نگران آن نباشید!</p>
<h2>استفاده از resetTransform</h2>
<p>نکته‌ی مهمی که باید به آن دقت کنید، استفاده از متد <code>resetTranform</code> یا به طور کلی، حذف تبدیلات است. از آنجایی که از این به بعد همیشه یک تغییر اندازه در برنامه اجرا شده و باید در طول برنامه حفظ شود، باید در استفاده از این متد و یا حذف تبدیلات دقت کافی داشته باشید، زیرا اگر این <code>scale</code> اولیه حذف شود، می‌تواند رفتار دیگر بخش‌های برنامه را نیز تحت تاثیر قرار دهد.</p>
<p>بهترین راه‌حل برای این مورد استفاده از متد‌های <code>save</code> و <code>restore</code> است، هرچند مواردی هستند که در آن‌ها استفاده از این متد‌ها ممکن نیست، اما در بیشتر موارد می‌توان از این متد‌ها استفاده کرد و مشکلی وجود ندارد.</p>
<h2>ترسیمات در صفحات واکنش‌گرا (responsive)</h2>
<p>یک مورد جالب توجه و مهم در canvas، مشکلاتی است که در صفحات واکنش‌گرا برای آن ایجاد می‌شود. در یک صفحه‌ی واکنش‌گرا تکلیف ترسیمات چیست؟ یک نمونه‌ی بسیار ساده را با هم بررسی می‌کنیم. در این نمونه اندازه‌ی عنصر همیشه برابر اندازه‌ی صفحه است:</p>
<pre><code class="language-css">
#cvs {
    width: 100vw;
    height: 100vh;

    position: absolute;
    top: 0; left: 0;
}
</code></pre>
<p>یک مورد دردسرساز این است که اندازه‌ی ترسیمات، شبیه به اندازه‌ی دیگر عناصر سند، با بزرگنمایی تغییر اندازه می‌دهند. کد زیر را در نظر بگیرید. در این کد که مشابه کد‌های بالاست، با بزرگنمایی صفحه، اندازه‌ی ترسیمات درون عنصر نیز بزرگ می‌شوند. کد زیر را همراه با کد‌های CSS بالا اجرا کنید:</p>
<pre><code class="language-javascript">
let cvs = document.getElementById("cvs"),
    ctx = cvs.getContext("2d"),
    
    random = (min, max) =&gt; ~~(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 &lt; 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 = () =&gt; {
    addEventListener("resize", () =&gt; {
        size_setup();
        initialize_canvas();

        draw_arcs();
    });

    size_setup();
    initialize_canvas();

    draw_arcs();
}

function initialize_canvas () {
    ctx.lineWidth = 3;
    ctx.lineCap = ctx.lineJoin = "round";
}
</code></pre>
<p>این مورد در برخی برنامه‌ها مشکل خاصی ایجاد نمی‌کند، اما برنامه‌های canvas بنا به طبیعت خود بهتر است اندازه‌ی ثابتی داشته باشند. در بیشتر برنامه‌هایی که تاکنون بررسی کرده‌ایم این مورد مهم است. در برخی برنامه‌ها، اگر ترسیمات بیش از حد بزرگ یا کوچک شوند، زیبایی طرح از بین می‌رود و فقط تبدیل به پردازش اضافی در مرورگر می‌شود!</p>
<p>راه‌حل این مشکل چیست؟ یک راه‌حل این است که اندازه‌ی عنصر را نسبت به اندازه‌ی صفحه‌ی نمایشگر تعیین کرده و هنگام بزرگنمایی، از تعیین‌اندازه‌ی مجدد خودداری کنیم، اما این مورد هنگام اجرا در رایانه‌های رومیزی (desktop) یک اشکال بزرگ دارد. کافیست اندازه‌ی کل مرورگر را به صورت دستی تغییر دهید!</p>
<p>راه‌حل بهتر چیست؟ بهترین کار این است که هنگام تغییر‌اندازه‌ی صفحه (بزرگنمایی یا تغییر اندازه) اندازه‌ی عنصر را دوباره تعیین کنیم. برای ثابت نگه داشتن اندازه‌ی ترسیمات می‌توان از اندازه‌ی کل صفحه یا عنصر استفاده کرد. شبیه به واحد‌های <code>vw</code> و <code>vh</code>، می‌توانیم یک درصد از اندازه‌ی صفحه‌نمایش (viewport) را ذخیره کرده و از آن به عنوان ضریب در ترسیمات استفاده کنیم.</p>
<p>برای نمونه به کد زیر دقت کنید. کد بالا را به صورت زیر اصلاح کرده و آن را اجرا کنید. با بزرگنمایی صفحه خواهید دید که اندازه‌ی ترسیمات، با وجود اجرای تابع <code>size_setup</code> هنگام تغییر اندازه، ثابت می‌ماند و تغییری نمی‌کند:</p>
<pre><code class="language-javascript">
let cvs = document.getElementById("cvs"),
    ctx = cvs.getContext("2d"),
    
    VW, VH,
    main_arc,

    random = (min, max) =&gt; ~~(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 &lt; 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 = () =&gt; {
    addEventListener("resize", () =&gt; {
        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);
}
</code></pre>
<p>روند کار برای دیگر حالت‌های واکنشگرایی نیز مانند کد بالاست، یعنی نیازی نیست برای هر اندازه‌ی صفحه، یک تابع جداگانه نوشته شود.</p>
<h2>نتیجه‌گیری</h2>
<p>در این بخش سعی کردیم مشکلات کار با canvas در پروژه‌های واقعی را بررسی کرده و برای بیشترشان راه‌حل‌هایی را بررسی کردیم. مشکل اصلی اینجاست که یک راه‌حل کامل و فراگیر برای این کار وجود ندارد و همه‌چیز به پروژه و همچنین برنامه‌ی نوشته‌شده برای canvas بستگی دارد، بنابراین سعی کردیم حالت‌های مختلف را بررسی کرده و راه‌حل‌هایی برای هرکدام معرفی کنیم.</p>
<p>این آموزش، آخرین قسمت از دوره‌ی آموزش canvas است. در طول این آموزش‌ها سعی بر آن بود که به ریزترین جزئیات هر مورد نیز پرداخته شود، اما مواردی نیز از این آموزش حذف شدند؛ از جمله برخی ویژگی‌ها مانند <code>filter</code> و ماتریس تبدیلات. علت این کار خودداری از پیچیدگی بیش‌از حد و همچنین سطح دانش شخص بنده و مخاطبان عزیز است.</p>
<p>احتمالا در دنباله‌ی این آموزش نیز مقالاتی خواهد بود ولی دیگر نمی‌توان نام «آموزش» روی آن‌ها گذاشت زیرا در آن‌ها بیشتر به پروژه‌های بزرگ خواهیم پرداخت. اگر فرصتی باشد، به موارد حذف‌شده در این دوره نیز خواهیم پرداخت، اما این مورد قطعی نیست.</p>
<p>نوشته <a rel="nofollow" href="https://css-tricks.ir/tutorial/%d8%aa%d8%b9%db%8c%db%8c%d9%86-%d8%a7%d9%86%d8%af%d8%a7%d8%b2%d9%87-canvas-%d9%88-%d8%aa%d8%b9%d8%a7%d9%85%d9%84-%d8%a2%d9%86-%d8%a8%d8%a7-css/">تعیین اندازه canvas و تعامل آن با CSS</a> اولین بار در <a rel="nofollow" href="https://css-tricks.ir">آموزش طراحی وب</a> پدیدار شد.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.ir/tutorial/%d8%aa%d8%b9%db%8c%db%8c%d9%86-%d8%a7%d9%86%d8%af%d8%a7%d8%b2%d9%87-canvas-%d9%88-%d8%aa%d8%b9%d8%a7%d9%85%d9%84-%d8%a2%d9%86-%d8%a8%d8%a7-css/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>کلاس Path2D قدرت جدید canvas</title>
		<link>https://css-tricks.ir/tutorial/%da%a9%d9%84%d8%a7%d8%b3-path2d-%d9%82%d8%af%d8%b1%d8%aa-%d8%ac%d8%af%db%8c%d8%af-canvas/</link>
					<comments>https://css-tricks.ir/tutorial/%da%a9%d9%84%d8%a7%d8%b3-path2d-%d9%82%d8%af%d8%b1%d8%aa-%d8%ac%d8%af%db%8c%d8%af-canvas/#respond</comments>
		
		<dc:creator><![CDATA[حسین رفیعی]]></dc:creator>
		<pubDate>Sat, 27 Nov 2021 03:42:49 +0000</pubDate>
				<category><![CDATA[آموزش]]></category>
		<category><![CDATA[canvas]]></category>
		<category><![CDATA[path]]></category>
		<category><![CDATA[path2d]]></category>
		<guid isPermaLink="false">https://css-tricks.ir/?p=9474</guid>

					<description><![CDATA[<p>در این آموزش به معرفی کلاس Path2D، متد‌های آن، و قدرتی که به canvas می‌افزاید پرداخته و چندین نمونه همراه با آن بررسی می‌کنیم.</p>
<p>نوشته <a rel="nofollow" href="https://css-tricks.ir/tutorial/%da%a9%d9%84%d8%a7%d8%b3-path2d-%d9%82%d8%af%d8%b1%d8%aa-%d8%ac%d8%af%db%8c%d8%af-canvas/">کلاس Path2D قدرت جدید canvas</a> اولین بار در <a rel="nofollow" href="https://css-tricks.ir">آموزش طراحی وب</a> پدیدار شد.</p>
]]></description>
										<content:encoded><![CDATA[<p>کلاس <code>Path2D</code> استانداردی است که سال‌ها بعد به canvas اضافه شد. پیش از این کلاس، روش کار با canvas تفاوت چندانی نداشت اما انجام کار‌های زیادی به دلیل محدودیت‌های شکل فعلی بسیار سخت یا غیرممکن بود. با معرفی این کلاس، هرچند تغییرات خاصی در روش کار با canvas ایجاد نشد، اما کارکرد آن دچار تغییرات اساسی شده و مفهوم «شکل فعلی» نیز به کلی تغییر کرد!</p>
<p>تاکنون تمام چیزی که آموخته‌اید به روش قدیمی، یا روش پیش از <code>Path2D</code> بوده است. اما در این آموزش روش کار با این کلاس، ویژگی‌های آن، و همچنین روش استفاده از آن در متد‌های canvas را خواهید آموخت. ممکن است به‌کار‌گیری آن در آغاز برایتان سخت باشد اما سعی کنید بیشتر از این کلاس استفاده کنید تا بتوانید بهتر از امکانات آن استفاده کنید.</p>
<h2>کارکرد این کلاس چیست؟</h2>
<p>به طور خلاصه، این کلاس یک شکل تعریف می‌کند. شئ ساخته‌شده با این کلاس یک شکل خالی درون خود دارد، و تمام متد‌های خط که در canvas هستند، برای این شئ نیز وجود دارند؛ یعنی تمام متد‌هایی که به شکل فعلی اثر می‌گذارند، روی این شئ نیز هستند و روی این شکل اثر می‌گذارند. به نمونه کد زیر دقت کنید. در کد زیر یک شئ <code>Path2D</code> ایجاد شده و از متد‌های مختلف آن برای تعریف شکل استفاده می‌شود:</p>
<pre><code class="language-javascript">
let path = new Path2D;

/* rect */
path.rect(10, 10, 100, 100);

/* arc */
path.arc(0, 0, 100, 0, Math.PI * 2);
path.ellipse(100, 0, 50, 25, 0, 0, Math.PI * 2);
path.arcTo(50, 50, 70, 30, 40);

/* line */
path.moveTo(0, 0);
path.lineTo(200, 200);
path.closePath();

/* curve */
path.quadraticCurveTo(180, 180, 150, 100);
path.bezierCurveTo(50, 80, 100, 80, 80, 120);
</code></pre>
<p>رفتار تمام متد‌های بالا مشابه متد‌های canvas است اما در اینجا، این متد‌ها به شکل درون شئ <code>path</code> اثر می‌گذارند. خب حال فرض کنیم یک شکل ساده تعریف کرده‌ایم. چطور می‌توانیم این شکل‌ها را رسم کنیم؟</p>
<h2>رسم یک شکل Path2D</h2>
<p>برای رسم شکل‌های تعریف‌شده با این کلاس باید دوباره نگاهی به ورودی‌های متد‌های <code>fill</code> و <code>stroke</code> بیاندازیم! پیش از این گفتیم که متد <code>stroke</code> هیچ ورودی‌ای ندارد و متد <code>fill</code> می‌تواند یک ورودی داشته باشد. اما این موضوع فقط برای روش قدیمی canvas درست است. در روش جدید، ورودی اول هرکدام از این متد‌ها شکل مورد‌نظر برای رسم است. کد زیر نمای کلی این متد‌ها را نشان می‌دهد:</p>
<pre><code class="language-javascript">
ctx.stroke(path);
ctx.fill(path, fill_rule);
</code></pre>
<p>اگر ورودی <code>path</code> برای این متد‌ها تعریف نشود، این متد‌ها شکل فعلی درون canvas را رسم می‌کنند. به نمونه کد زیر دقت کنید. در کد زیر یک شکل <code>Path2D</code> تعریف شده و توسط متد <code>fill</code> رسم می‌شود، سپس این متد بدون شکل ورودی اجرا می‌شود که باعث رسم شکل فعلی عنصر (با شفافیت) می‌شود:</p>
<pre><code class="language-javascript">
let my_path = new Path2D;

my_path.moveTo(50, 50);
my_path.lineTo(150, 150);

my_path.lineTo(150, 50);
my_path.lineTo(50, 150);

my_path.closePath();

ctx.fillStyle = "#3D3"; /* GREEN */
ctx.fill(my_path);

ctx.arc(80, 50, 40, 0, Math.PI * 2);

ctx.fillStyle = "rgba(200, 30, 200, 0.3)";
ctx.fill();
</code></pre>
<h2>ورودی دیگر متد‌ها</h2>
<p>به جز متد‌های <code>fill</code> و <code>stroke</code>، سه متد دیگر نیز هستند که به شکل فعلی مربوط هستند و می‌توانند یک شکل <code>Path2D</code> به عنوان ورودی داشته باشند. این مورد فقط به تعداد ورودی‌های این متد‌ها مربوط است و هیچ تغییری در رفتار این متد‌ها ندارد. در ادامه به هر یک از این متد‌ها می‌پردازیم.</p>
<h3>متد clip</h3>
<p>این متد در حالت قدیمی یک ورودی اختیاری دارد که همان <code>fill_rule</code> است که شبیه ورودی متد <code>fill</code> است و می‌تواند رفتار محدوده در شکل‌های پیچیده را مشخص کند. اگر از مقدار <code>"evenodd"</code> برای این ورودی استفاده شود، رفتار این متد تغییر کرده و بخش‌هایی از شکل که روی هم می‌افتند، جزء محدوده نخواهند بود. برای درک بهتر این مقدار در این متد، شکل مورد‌نظر خود را با الگوی even-odd رسم کنید. بخش‌هایی از شکل که رسم نشده‌اند، در متد <code>clip</code> نیز جزئی از محدوده‌ی ترسیمات نخواهند بود. علت اینکه پیش از این به این ورودی اشاره نکردیم، پیچیدگی بیش‌از‌حد آن است اما الان برای فهمیدن آن دانش و تسلط کافی دارید!</p>
<p>این متد در حالت جدید دو ورودی می‌پذیرد که ورودی اول همان شکل <code>Path2D</code> و ورودی دوم نیز <code>fill_rule</code> است. این متد ترسیمات canvas را به این شکل ورودی محدود می‌کند. اگر این ورودی داده نشود، از شکل فعلی خود عنصر استفاده می‌شود. به کد زیر دقت کنید. در کد زیر دو شکل تعریف می‌شوند، سپس از یکی درون متد <code>clip</code> و از دیگری برای رسم استفاده می‌شود:</p>
<pre><code class="language-javascript">
cvs.width = cvs.height = 500;

let clip_path = new Path2D,
    draw_path = new Path2D;

clip_path.moveTo(200, 0);
clip_path.lineTo(550, 300);

clip_path.lineTo(50, 450);
clip_path.closePath();

draw_path.arc(250, 250, 200, 0, Math.PI * 2);

ctx.save();
ctx.clip(clip_path);

ctx.fillStyle = "#3D3"; /* GREEN */
ctx.fill(draw_path);

ctx.restore();

ctx.setLineDash([20]);
ctx.lineWidth = 3;

ctx.lineCap = ctx.lineJoin = "round";
ctx.stroke(draw_path);
</code></pre>
<p>می‌توانیم یک برنامه‌ی پیچیده‌تر بنویسیم. در کد زیر دو شکل شامل چندین دایره با مختصات و شعاع تصادفی ایجاد می‌شوند، سپس ترسیمات به یک شکل محدود شده و شکل دیگر رسم می‌شود. در کد زیر مقدار <code>fill_rule</code> برای متد <code>clip</code> برابر <code>"evenodd"</code> است. کد زیر را بررسی کرده و سعی کنید ویژگی‌های بیشتری به آن اضافه کنید:</p>
<pre><code class="language-javascript">
function get_random (min, max) {
    return Math.random() * (max - min + 1) + min;
}

function generate_arc (path, n, min, max) {
    let i, x, y, r;
    
    for (i = 0; i &lt; n; i++) {
        x = get_random(0, cvs.width);
        y = get_random(0, cvs.height);
        r = get_random(min, max);
        
        path.moveTo(x + r, y);
        path.arc(x, y, r, 0, Math.PI * 2);
    }
}

function random_color () {
    return `rgb(${get_random(0, 255)},
                ${get_random(0, 255)},
                ${get_random(0, 255)}`;
}

let clip_path = new Path2D,
    draw_path = new Path2D;

generate_arc(clip_path, 8, 80, 100);
generate_arc(draw_path, 40, 20, 40);

ctx.lineWidth = 2;
ctx.lineCap = ctx.lineWidth = "round";

ctx.save();
ctx.clip(clip_path, "evenodd");

ctx.fillStyle = random_color();
ctx.fill(draw_path, "evenodd");

ctx.restore();
ctx.stroke(clip_path);

ctx.setLineDash([15]);
ctx.stroke(draw_path);
</code></pre>
<p><img decoding="async" src="https://css-tricks.ir/wp-content/uploads/2021/10/canvas-random-clip.png" alt="canvas random clip" /><br />
در کد بالا تابع <code>get_random</code> شبیه به توابع قبلی یک عدد تصادفی در محدوده‌ی ورودی‌هایش برمی‌گرداند، تابع <code>random_color</code> نیز یک رنگ تصادفی به فرمت <code>rgb</code> برمی‌گرداند. تابع <code>generate_arc</code> نیز چهار ورودی می‌پذیرد که ورودی اول شکل مورد‌نظر، ورودی دوم تعداد دایره‌ها، و ورودی‌های سوم و چهارم محدوده‌ی اندازه‌ی شعاع هستند.</p>
<p>درون این تابع، مختصات و شعاع دایره‌ها تعیین شده و درون شکل قرار می‌گیرند. البته از آنجایی که چند دایره در شکل وجود دارند، از متد <code>moveTo</code> همراه با آن استفاده شده تا از ایجاد خطوط اضافی جلوگیری کند. در نهایت پس از اینکه دو شکل با استفاده از این تابع ایجاد شدند، لایه‌ی ترسیمات به شکل <code>clip_path</code> محدود شده و شکل <code>draw_path</code> رسم می‌شود. پس از انجام این کار، به هر دو شکل حاشیه داده می‌شود.</p>
<h3>متد isPointInPath</h3>
<p>این متد نیز در حالت قدیمی خود سه ورودی دارد، اما در حالت جدید چهار ورودی می‌پذیرد. ورودی اول شکل مورد‌نظر، ورودی‌های دوم و سوم مختصات نقطه، و ورودی چهارم مقدار <code>fill_rule</code> است. نمای کلی این متد به صورت زیر است:</p>
<pre><code class="language-javascript">
ctx.isPointInPath(path, x, y, fill_rule);
</code></pre>
<h3>متد isPointInStroke</h3>
<p>این متد نیز شبیه به متد قبل است با این تفاوت که مقدار <code>fill_rule</code> را ندارد. نمای کلی این متد به صورت زیر است:</p>
<pre><code class="language-javascript">
ctx.isPointInStroke(path, x, y);
</code></pre>
<p>به نمونه کد زیر دقت کنید. در کد زیر دو شکل تصادفی درون هم ایجاد شده‌اند. بخش‌هایی که بین دو شکل است رسم نشده اما دیگر بخش‌های شکل بزرگ‌تر رسم شده. هر دو شکل دارای حاشیه هستند. با هر حرکت موس روی عنصر، مختصات نشانگر موس بررسی می‌شود. اگر روی حاشیه‌ی شکل قرار داشت، رنگ درون شکل به یک رنگ تصادفی تعیین می‌شود. همچنین با کلیک روی عنصر، شکل‌ها تغییر می‌کنند:</p>
<pre><code class="language-javascript">
function get_random (min, max) {
    return Math.random() * (max - min + 1) + min;
}

function random_color () {
    return `rgb(${get_random(0, 255)},
                ${get_random(0, 255)},
                ${get_random(0, 255)}`;
}

function polar (n, min, max) {
    let path = new Path2D,
        angle = 2 * Math.PI / n,
        i, r, x, y;
    
    for (i = 0; i &lt; n; i++) {
        r = get_random(min, max);
        x = r * Math.cos(i * angle);
        y = r * Math.sin(i * angle);

        path.lineTo(x, y);
    }

    path.closePath();
    return path;
}

let big_path,
    small_path;

ctx.lineWidth = 3;
ctx.lineCap = ctx.lineJoin = "round";

ctx.setLineDash([80, 20])
ctx.translate(250, 250);

function new_path () {
    big_path = polar(100, 210, 240);
    small_path = polar(50, 70, 100);
    
    draw_path();
}

function draw_path () {
    ctx.save();
    ctx.resetTransform();
    
    ctx.clearRect(0, 0, 500, 500);
    ctx.restore();
    
    ctx.fillStyle = random_color();
    ctx.fill(big_path);
    
    ctx.save();
    ctx.globalCompositeOperation = "destination-out";
    
    ctx.fill(small_path);
    ctx.restore();
    
    ctx.stroke(big_path);
    ctx.stroke(small_path);
}

function check_cursor (e) {
    let box = cvs.getBoundingClientRect(),
        
        x = e.clientX - box.left,
        y = e.clientY - box.top;
    
    if (ctx.isPointInStroke(big_path, x, y) ||
        ctx.isPointInStroke(small_path, x, y)
    ) {
        draw_path();
    }
}

cvs.addEventListener("click", new_path);
cvs.addEventListener("mousemove", check_cursor);

new_path();
</code></pre>
<p>کد بالا به نظر پیچیده است اما تمام موارد آن پیش از این بررسی شده‌اند و نیاز به توضیح خاصی ندارد. سعی کنید تغییرات خود را در کد ایجاد کرده و ویژگی‌های تازه‌ای به آن اضافه کنید.</p>
<h2>ویژگی‌های این کلاس</h2>
<p>اشیاء ساخته‌شده با این کلاس به جز متد‌های گفته‌شده هیچ ویژگی یا متد دیگری (به جز یک مورد) ندارند، اما رفتار آن‌ها در canvas دارای ویژگی‌هایی است که دانستن آن‌ها مهم است.</p>
<ul>
<li>اول اینکه این اشیاء فقط حکم یک شکل را دارند و این شکل هیچ ویژگی‌ای ندارد. یعنی این اشیاء ویژگی‌هایی مانند رنگ، اندازه‌ی حاشیه، خط‌چین و&#8230; ندارند، بلکه زمینه‌ی canvas این ویژگی‌ها را دارد و به شکل اعمال می‌کند.</li>
<li>دوم اینکه پس از تعریف یک شکل، امکان پاک کردن حافظه‌ی آن شکل وجود ندارد. این یعنی برای هر ترسیمی لازم است یک شکل جدید تعریف و استفاده شود! این مورد به‌ویژه در انیمیشن‌ها و پویانمایی بسیار آزاردهنده است زیرا ممکن است در هر فریم تعریف یک شکل جدید نیاز باشد!</li>
<li>سوم اینکه متن و تصویر نمی‌توانند بخشی از این شکل‌ها باشند. زیرا این موارد حتی در canvas نیز وارد شکل فعلی نمی‌شوند و برای اشیاء ساخته‌شده با این کلاس نیز همین وضعیت وجود دارد. برای نوشتن متن یا رسم تصویر فقط می‌توان از متد‌های زمینه استفاده کرد.</li>
</ul>
<h2>برتری‌های این کلاس</h2>
<p>به جز مواردی که جزء اشکالات یا سختی‌های کار با این کلاس هستند، این کلاس برتری‌های خاص خود را دارد که نمی‌توان از آن‌ها چشم‌پوشی کرد. برای نمونه سرعت بالا از جمله این موارد است. جدا کردن شکل از حافظه‌ی canvas به کمک این کلاس باعث شده که سرعت ایجاد ترسیمات، به‌ویژه در انیمیشن‌ها، بسیار بیشتر شود و انجام بسیاری از کار‌ها نیز ممکن شود.</p>
<p>پیش از این کلاس، حتی اجرای متد <code>isPointInPath</code> روی یک شکل جدا از شکل فعلی بسیار دردسر‌ساز بود و می‌توانست سرعت برنامه را بسیار پایین بیاورد. اما به کمک این کلاس می‌توان متد‌های مربوط به شکل را روی چندین شکل مختلف اجرا کرد و هیچ مشکلی در سرعت برنامه پیش نیاید.</p>
<h2>موارد خاص</h2>
<p>شاید با خود فکر کنید که با وجود این کلاس دیگر نیازی به استفاده از شکل فعلی نیست. خب درست فکر می‌کنید! اما در مواردی که نیازی به تعریف بیش از یک شکل نیست، می‌توان از شکل فعلی درون canvas استفاده کرد. در این موارد تفاوت خاصی در سرعت برنامه وجود ندارد و همان نتیجه به دست می‌آید.</p>
<h2>ورودی‌های این کلاس</h2>
<p>پیش از این، از کلاس <code>Path2D</code> بدون اینکه برای آن ورودی تعیین کنیم برای ساخت شکل استفاده می‌کردیم، اما این کلاس دو حالت دیگر نیز دارد که می‌توانند بسیار کاربردی باشند و به بررسی آن‌ها می‌پردازیم:</p>
<pre><code class="language-javascript">
let path_from_existing = new Path2D(path),
    path_from_svg = new Path2D(d);
</code></pre>
<p>حالت اول این است که یک شئ <code>Path2D</code> دیگر به عنوان ورودی این کلاس داده شود، در این صورت یک پیوست (کپی) از شکل ورودی در شکل تازه قرار می‌گیرد. به نمونه کد زیر دقت کنید. در کد زیر ابتدا یک دایره تعریف می‌شود و در شکل <code>old_path</code> ذخیره می‌شود، سپس یک شکل جدید به نام <code>new_path</code> از روی شکل قبلی ایجاد شده و در canvas رسم می‌شود. همانطور که خواهید دید، شکل قبلی درون شکل جدید قرار گرفته است:</p>
<pre><code class="language-javascript">
let old_path = new Path2D;
old_path.arc(200, 200, 100, 0, Math.PI * 2);

let new_path = new Path2D(old_path);
old_path.rect(100, 100, 200, 200);

ctx.lineWidth = 3;
ctx.setLineDash([30, 15]);
ctx.lineCap = ctx.lineJoin = "round";

ctx.fillStyle = "#3D3"; /* GREEN */
ctx.strokeStyle = "#111"; /* BLACK */

ctx.fill(new_path);
ctx.stroke(old_path);
</code></pre>
<p>کد بالا به نکته‌ی مهم دیگری نیز اشاره می‌کند. همانطور که می‌بینید، پس از تعریف <code>new_path</code>، یک مربع دیگر نیز به شکل <code>old_path</code> اضافه شد اما این مربع درون <code>new_path</code> قرار نگرفت. این یعنی شکل‌هایی که درون یکدیگر پیوست می‌شوند، فقط هنگام تعریف به هم وابسته هستند و پس از آن، تغییر در شکل قبلی، تاثیری در دیگر شکل‌ها ندارد.</p>
<p>در حالت دوم این کلاس، ورودی یک متن است و این متن شبیه به مقدار ویژگی <code>d</code> در عنصر <code>path</code> در SVG است! اگر به یاد داشته باشید، در آموزش <a href="https://css-tricks.ir/?p=9325">شکل‌های پایه</a> گفتیم که دستورات <code>path</code> در ترسیمات canvas نیز کاربرد دارد و بهتر است آن‌ها را نیز یاد بگیرید. کاربرد این دستورات در ورودی این کلاس است. به کمک این کلاس می‌توان با استاندارد SVG شکل تعریف کرد و از آن شکل درون canvas استفاده کرد.</p>
<p>برای نمونه به کد زیر دقت کنید. در کد زیر از دستورات متنی برای ایجاد شکل استفاده شده است. همچنین تابع <code>polar</code> تغییر یافته و شکلی که ایجاد می‌کند، ابتدا به دستور <code>d</code> تبدیل شده و سپس درون کلاس <code>Path2D</code> قرار می‌گیرد، البته شکلی که ایجاد می‌شود تغییری نکرده، بلکه فقط نوع ایجاد آن تغییر کرده:</p>
<pre><code class="language-javascript">
function get_random (min, max) {
    return Math.random() * (max - min + 1) + min;
}

function random_color () {
    return `rgb(${get_random(0, 255)},
                ${get_random(0, 255)},
                ${get_random(0, 255)}`;
}

let a_path = new Path2D("M 50 50, 350 350, 350 50, 50 350, Z");

ctx.lineWidth = 10;
ctx.lineCap = ctx.lineJoin = "round";

ctx.strokeStyle = random_color();
ctx.stroke(a_path);

function polar (n, min, max) {
    let d = [],
        angle = Math.PI * 2 / n,
        i, r, x, y;
    
    for (i = 0; i &lt; n; i++) {
        r = get_random(min, max);

        x = r * Math.cos(i * angle);
        y = r * Math.sin(i * angle);

        d.push(x, y);
    }

    d = "M " + d.join(" ") + " Z";
    return new Path2D(d);
}

let d_path = polar(50, 150, 170);

ctx.fillStyle = random_color();
ctx.translate(250, 250);

ctx.fill(d_path);
</code></pre>
<p>شاید با خود بگویید که تابع <code>polar</code> قبلی نیز همین کار را انجام می‌دهد. پس چه نیازی به استفاده از ورودی متن داریم؟ پاسخ در دستورات متنی است، زیرا canvas معادلی برای برخی از این دستورات ندارد و ورودی متنی در این موارد نسبت به متد‌های canvas برتری دارد.</p>
<p>از جمله‌ی این موارد می‌توان به دستورات <code>S</code> و <code>T</code> همراه با دستورات <a href="https://css-tricks.ir/?p=6746#quadratic-bezier">Q</a> و <a href="https://css-tricks.ir/?p=6746#cubic-bezier">C</a> اشاره کرد. همانطور که در آموزش <a href="https://css-tricks.ir/?p=9325">شکل‌های پایه</a> گفتیم، در canvas متد معادلی برای این دو دستور نیست. مورد دیگری که به آن اشاره کردیم، مختصات نسبی است. گفتیم در canvas تمام مختصات مطلق هستند، اما در ورودی متنی می‌توان از مختصات نسبی شبیه به SVG استفاده کرد. ممکن است ترسیماتی داشته باشیم که یا با استاندارد SVG تعریف شده‌اند، یا مختصات نسبی دارند، یا اینکه چند خم پشت سر هم دارند و استفاده از دستورات S و T در آن‌ها بهتر است. در چنین مواردی، ورودی متنی بسیار کاربردی است.</p>
<h2>بررسی یک نمونه</h2>
<p>در اینجا سعی می‌کنیم روش ایجاد شکل تصادفی را گسترش داده و شکل‌های تصادفی را به صورت انیمیشن درآوریم. برای درک روش کار بهتر است با مختصات قطبی، که پیش از این نیز به آن پرداخته‌ایم، آشنایی داشته باشید. همچنین آشنایی با توابع مثلثاتی نیز به درک بهتر آن کمک می‌کند.</p>
<p>می‌دانیم که برای ساختن شکل‌های تصادفی، استفاده از مختصات قطبی بهتر است. در کد زیر تابع <code>polar</code> یک شکل تصادفی به مرکز <code>(cx,cy)</code> و در بازه‌ی <code>min</code> و <code>max</code> ایجاد می‌کند. از آنجایی که در مواردی لازم است اعداد تصادفی با بخش اعشاری داشته باشیم، تابع <code>random</code> را نیز تغییر دهید. می‌توانیم برای سادگی به جای نام <code>get_random</code> از نام <code>random</code> استفاده کنیم:</p>
<pre><code class="language-javascript">
function random (min, max) {
    return ((Math.random() * (max - min + 1) * 1000 | 0) / 1000) + min;
}

function polar (n, cx, cy, min, max) {
    let vertices = [],
        angle = Math.PI * 2 / n,
        i, x, y, r, t;
    
    for (i = 0; i &lt; n; i++) {
        r = random(min, max);
        t = i * angle;

        x = cx + r * Math.cos(t);
        y = cy + r * Math.sin(t);

        vertices.push(x, y);
    }

    return vertices;
}
</code></pre>
<p>یک روش ایجاد انیمیشن روی یک شکل، این است که هرکدام از نقاط آن شکل دارای سرعت تصادفی باشند. برای این منظور یک کلاس به نام <code>Vertex</code> ایجاد می‌کنیم که یک نقطه از هر شکل را درون خود دارد و سرعتی تصادفی برای آن ایجاد می‌کند. ویژگی <code>coords</code> این کلاس مختصات نقطه را برمی‌گرداند و متد <code>update</code> این کلاس نیز مختصات را نسبت به سرعتشان به‌روز‌رسانی می‌کند. البته اگر نقاط با دیواره‌های صفحه برخورد کنند، با حالتی بازتابی برمی‌گردند:</p>
<pre><code class="language-javascript">
class Vertex
{
    constructor (x, y) {
        this.x = x;
        this.y = y;

        this.speed = [
            random(-1, 1),
            random(-1, 1)
        ];
    }

    get coords () {
        return [this.x, this.y];
    }
}

Vertex.update = function (vertex) {
    vertex.x = Math.max(0, Math.min(cvs.width, vertex.x + vertex.speed[0]));
    vertex.y = Math.max(0, Math.min(cvs.height, vertex.y + vertex.speed[1]));

    if (vertex.x === 0 || vertex.x === cvs.width) vertex.speed[0] *= -1;
    if (vertex.y === 0 || vertex.y === cvs.height) vertex.speed[1] *= -1;
}
</code></pre>
<p>حال برای استفاده از این کلاس لازم است تابع <code>polar</code> را تغییر داده و یک تابع برای انیمیشن ایجاد کنیم. به این منظور تابع <code>draw_shape</code> را ایجاد کرده و در ادامه برنامه را برای اجرا آماده می‌کنیم:</p>
<pre><code class="language-javascript">
cvs.width = cvs.height = 500;

function polar (n, cx, cy, min, max) {
    let vertices = [],
        angle = Math.PI * 2 / n,
        i, x, y, r, t;
    
    for (i = 0; i &lt; n; i++) {
        r = random(min, max);
        t = i * angle;

        x = cx + r * Math.cos(t);
        y = cy + r * Math.sin(t);

        vertices.push(new Vertex(x, y));
    }

    return vertices;
}

function clear (context) {
    context.save();

    context.globalCompositeOperation = "copy";
    context.fillStyle = "rgba(0, 0, 0, 0)";

    context.fillRect(0, 0, 1, 1);
    context.restore();
}

function draw_shape () {
    ctx.beginPath();
    clear(ctx);

    for (let i = 0, l = vertices.length; i &lt; l; i+=2) {
        ctx.lineTo(...vertices[i].coords);
        Vertex.update(vertices[i]);
    }

    ctx.closePath();

    ctx.fill("evenodd");
    ctx.stroke();

    requestAnimationFrame(draw_shape);
}

let vertices = polar(40, cvs.width / 2, cvs.height / 2, 200, 220);

ctx.lineCap = ctx.lineJoin = "round";
ctx.lineWidth = 5;

ctx.fillStyle = "#3D3"; /* GREEN */
draw_shape();
</code></pre>
<p>با اجرای برنامه متوجه اشکال این روش خواهید شد. شکل در آغاز برنامه حالت مناسبی دارد، اما با حرکت نقاط، حالت خود را از دست می‌دهد. ممکن است در برخی موارد این نوع حرکت مناسب باشد، اما این چیزی نیست که ما می‌خواهیم! خواسته‌ی ما این است که شکل‌ها یک‌دست باقی بمانند و حالت خود را از دست ندهند. برای این کار لازم است همچنان از مختصات قطبی برای ایجاد تغییر در نقاط استفاده کنیم. برای این کار تغییراتی در تابع <code>polar</code> و کلاس <code>Vertex</code> ایجاد می‌کنیم:</p>
<pre><code class="language-javascript">
class Vertex
{
    constructor (r, t) {
        this.r = r;
        this.t = t;

        this.angle = random(0, Math.PI * 2);
        this.angle_speed = random(10, 100) / 1000;

        this.speed = random(10, 20);
        this.nr = r;
    }

    get coords () {
        return [this.r * Math.cos(this.t), this.r * Math.sin(this.t)];
    }
}

Vertex.update = function (vertex) {
    vertex.r = vertex.nr + Math.sin(vertex.angle) * vertex.speed;
    vertex.angle = (vertex.angle + vertex.angle_speed) % (Math.PI * 2);
}

function polar (n, min, max) {
    let vertices = [],
        angle = Math.PI * 2 / n,
        i, r, t;
    
    for (i = 0; i &lt; n; i++) {
        r = random(min, max);
        t = i * angle;

        vertices.push(new Vertex(r, t));
    }

    return vertices;
}

let vertices = polar(40, 200, 220);

ctx.lineCap = ctx.lineJoin = "round";
ctx.lineWidth = 5;

ctx.fillStyle = "#3D3"; /* GREEN */
ctx.translate(cvs.width / 2, cvs.height / 2);

draw_shape();
</code></pre>
<p>کد خود را اصلاح کرده و نتیجه را ببینید. همانطور که می‌بینید شکل یک‌دست باقی می‌ماند اما همچنان تغییر می‌کند و حالت انیمیشنی و پویا دارد. حال به روش کار کلاس <code>Vertex</code> می‌پردازیم. هنگام ایجاد یک شئ با این کلاس، دو مقدار r و t وارد آن می‌شوند که شعاع و زاویه‌ی نقطه در مختصات قطبی هستند. سپس ویژگی‌های <code>speed</code>، <code>angle</code>، <code>angle_speed</code> به صورت تصادفی برای این نقطه ایجاد می‌شوند. ویژگی <code>nr</code> نیز همان مقدار شعاع را درون خود خواهد داشت.</p>
<p>شئ ساخته شده با این کلاس یک ویژگی دیگر به نام <code>coords</code> نیز دارد که به صورت getter تعریف شده است. هنگام دریافت این ویژگی، مختصات دکارتی آن نقطه برگردانده می‌شود. از این ویژگی هنگام رسم نقطه استفاده می‌شود. در تابع <code>draw_shape</code> شبیه به کد قبل متد <code>update</code> روی تمام نقاط اجرا می‌شود و آن نقاط درون شکل قرار می‌گیرند.</p>
<p>پیچیده‌ترین بخش این کد، تابع <code>update</code> است. در این تابع، شعاع نقطه بر اساس مقدار <code>speed</code> و <code>angle</code> تغییر می‌کند. از مقدار <code>nr</code> به این منظور استفاده می‌شود که مقدار اولیه‌ی شعاع حفظ شود. همانطور که می‌دانید توابع <code>sin</code> و <code>cos</code> توابعی متناوب هستند، یعنی رفتارشان در بازه‌های مشخص تکرارشوند است. ما از این ویژگی تابع <code>sin</code> استفاده کرده و حاصل ضرب آن در <code>speed</code> را به شعاع اضافه کردیم. مقدار این حاصل‌ضرب در بازه‌ی <code>[-speed,speed]</code> است و از آنجایی که تابع <code>sin</code> متناوب است، مقدار شعاع نیز به صورت متناوب تغییر می‌کند، یعنی پس از مدتی به حالت اولیه برگشته و رفتار خود را تکرار می‌کند.</p>
<p>توابع <code>sin</code> و <code>cos</code> در بازه‌ی [0,2π] متناوب هستند پس یعنی می‌توانیم زاویه <code>angle</code> را در این بازه نگه داریم، بدون اینکه نتیجه تغییر کند. به همین دلیل از <code>% (Math.PI * 2)</code> استفاده کردیم. در هر دوره‌ی تناوب، شعاع نقطه در یک بازه‌ی مشخص تغییر می‌کند. ترکیب رفتار این نقاط در یک شکل باعث ایجاد رفتار تغییر شکل می‌شود. برای درک بهتر این موضوع، کد زیر را اجرا کنید. در کد زیر به جای رسم شکل کلی، شعاع هر نقطه رسم می‌شود. به تغییرات هر شعاع دقت کنید؛ همانطور که می‌بینید، رفتار آن‌ها تکراری و متناوب است:</p>
<pre><code class="language-javascript">
function draw_shape () {
    ctx.beginPath();
    clear(ctx);

    for (let i = 0, l = vertices.length; i &lt; l; i+=2) {
        ctx.moveTo(0, 0);
        ctx.lineTo(...vertices[i].coords);
        Vertex.update(vertices[i]);
    }
    
    ctx.stroke();
    requestAnimationFrame(draw_shape);
}
</code></pre>
<h3>ایجاد شکل‌های خمیده</h3>
<p>ممکن است از خود پرسیده باشید که چرا از <code>Path2D</code> در این کد استفاده نشد؟ آن هم در آموزشی که مربوط به این کلاس است؟! مشکلی نیست! می‌توانیم از این کلاس نیز استفاده کنیم. برای این کار تابع <code>draw_shape</code> را به این شکل تغییر می‌دهیم. در کد جدید این تابع، ابتدا ویژگی <code>d</code> تعریف شده و در نهایت درون ورودی یک شئ <code>Path2D</code> قرار گرفته و در نهایت این شکل رسم می‌شود:</p>
<pre><code class="language-javascript">
function draw_shape () {
    clear(ctx);

    let l = vertices.length,
        d = [], i, path;

    for (i = 0; i &lt; l; i+=2) {
        d.push(...vertices[i].coords);
        Vertex.update(vertices[i]);
    }

    d = "M " + d.join(" ") + " Z";
    path = new Path2D(d);

    ctx.fill(path);
    ctx.stroke(path);

    requestAnimationFrame(draw_shape);
}
</code></pre>
<p>چطور می‌توانیم یک شکل خمیده با این نقاط ایجاد کنیم؟ منظور این است که شکل ایجاد‌شده دارای لبه‌های تیز نباشد، بلکه لبه‌های صاف داشته باشد. برای این کار می‌توانیم از دستور <code>S</code> بهره بگیریم. اگر بخواهیم می‌توانیم از متد <code>bezierCurveTo</code> نیز استفاده کنیم، اما دردسر آن بسیار بیشتر است. اگر آموزش مربوط به دستور <code>S</code> را به خوبی مطالعه کرده باشید، می‌دانید که نیازی نیست حتما پیش از آن از دستور <code>C</code> استفاده کرده باشیم.</p>
<p>برای انجام این کار باید درک خوبی از خم‌ها و روش کار دستور <code>S</code> داشته باشیم. برای اینکه خم‌ها به درستی روی نقاط آرایه ایجاد شوند، باید نقاط دیگری نیز وارد معادله کنیم، این نقاط، بین نقاط آرایه قرار دارند. از آنجایی که این نقاط فقط برای ایجاد شکل استفاده می‌شوند و کاربرد دیگری ندارند، فقط به مختصات آن‌ها نیاز داریم، بنابراین می‌توانیم متد <code>middle</code> را به کلاس <code>Vertex</code> اضافه کنیم. این متد مختصات میان دو نقطه‌ی ورودی خود را برمی‌گرداند:</p>
<pre><code class="language-javascript">
Vertex.middle = function (v1, v2) {
    let c1 = v1.coords,
        c2 = v2.coords;
    
    return [(c1[0] + c2[0]) / 2, (c1[1] + c2[1]) / 2];
}
</code></pre>
<p>همچنین برای رسم خم، یک تابع جدید ایجاد می‌کنیم. این تابع شبیه به تابع <code>draw_shape</code> است و نام آن را <code>draw_curve</code> می‌گذاریم. شکل کلی این تابع به صورت زیر است. هدف نهایی ما این است که یک خم یک‌دست و پیوسته با این نقاط، به کمک دستور <code>S</code>، ایجاد کنیم:</p>
<pre><code class="language-javascript">
function draw_curve () {
    clear(ctx);

    let l = vertices.length,
        d = [], i, path;

    /* (*) */

    for (i = 0; i &lt; l; i++) {
        /* DRAW CURVE */
        Vertex.update(vertices[i]);
    }

    d = "M " + d.join(" S ");
    path = new Path2D(d);

    ctx.fill(path);
    ctx.stroke(path);

    requestAnimationFrame(draw_curve);
}
</code></pre>
<p>در کد بالا، به جای توضیحات درون حلقه، باید دستورات مناسب را وارد <code>d</code> کنیم. در ادامه و پس از پایان حلقه، دستورات درون <code>d</code> به شکل یک متن استاندارد تبدیل شده و یک شکل با آن‌ها ساخته می‌شود. در نهایت این شکل در canvas رسم می‌شود. از آنجایی که نقاط خم باید به هم برسند، نیازی به استفاده از دستور <code>Z</code> در انتهای <code>d</code> نیست.</p>
<p>برای داشتن یک خم پیوسته و بدون شکستگی، باید از دستور <code>d</code> برای مختصات نقطه‌ی فعلی، و مختصات بین نقطه‌ی فعلی و نقطه‌ی بعدی استفاده کنیم. به این منظور کد درون حلقه را به این شکل تغییر می‌دهیم:</p>
<pre><code class="language-javascript">
for (i = 0; i &lt; l; i++) {
    d.push(vertices[i].coords.join(" ") + " " + Vertex.middle(vertices[i], vertices[(i + 1) % l]).join(" "));
    Vertex.update(vertices[i]);
}
</code></pre>
<p>یک پرسش مهم که با دیدن کد بالا ایجاد می‌شود، این است که چرا به جای <code>i + 1</code> از عبارت <code>(i + 1) % l</code> استفاده شد؟ می‌دانیم که باید مختصات میان هر نقطه، با نقطه‌ی پس از آن محاسبه شود. خب، وقتی حلقه به مرحله‌ی <code>i = l - 1</code> می‌رسد، یعنی حلقه به آخرین نقطه رسیده، و باید مختصات بین نقطه‌ی آخر و نقطه‌ی اول محاسبه شود. خب اندیس نقطه‌ی اول در آرایه <code>0</code> است. وقتی از عبارت <code>(i + 1) % l</code> استفاده کنیم و <code>i = l - 1</code> باشد، عبارت <code>(i + 1) % l</code> برابر صفر می‌شود، یعنی همان اندیس نقطه‌ی اول! علت استفاده از این عبارت همین است. همچنین اگر از این عبارت استفاده نشود با خطا مواجه خواهید شد، اگر به ساختار کد دقت کنید علت آن بدیهی است!</p>
<p>خب برنامه را اجرا کنید. همانطور که می‌بینید یک خم یک‌دست و پیوسته ایجاد شده که در حال تغییر است. به جز یک نقطه! این نقطه همان نقطه‌ی اولیه‌ی خم است که یک لبه‌ی تیز ایجاد کرده. برای حل این مشکل لازم است که مختصات قلم (به جای توضیحات <code>*</code> پیش از آغاز حلقه) به مختصات بین نقطه‌ی آخر و اول برده شود. بنابراین پیش از اجرای حلقه، این کد را به تابع اضافه می‌کنیم:</p>
<pre><code class="language-javascript">
d.push(Vertex.middle(vertices[l - 1], vertices[0]));
</code></pre>
<p>به جای بردن قلم به مختصات میان نقطه‌ی اول و آخر، می‌توانستیم آن را بین هر دو نقطه‌ی دیگری (مثلا نقطه‌ی اول و دوم) نیز قرار دهیم و مشکل خم حل شود، اما با این کار مجبور بودیم کد حلقه را نیز تغییر دهیم تا برنامه با خطا مواجه نشده و همه‌ی نقاط به‌روز‌رسانی شوند. کد کامل این تابع به صورت زیر است:</p>
<pre><code class="language-javascript">
function draw_curve () {
    clear(ctx);

    let l = vertices.length,
        d = [], i, path;

    d.push(Vertex.middle(vertices[l - 1], vertices[0]));

    for (i = 0; i &lt; l; i++) {
        d.push(vertices[i].coords.join(" ") + " " + Vertex.middle(vertices[i], vertices[(i + 1) % l]).join(" "));
        Vertex.update(vertices[i]);
    }

    d = "M " + d.join(" S ");
    path = new Path2D(d);

    ctx.fill(path);
    ctx.stroke(path);

    requestAnimationFrame(draw_curve);
}
</code></pre>
<p>در شکل زیر سه مجموعه شکل به کمک توابع <code>draw_shape</code> و <code>draw_curve</code> رسم شده‌اند. با وجود اینکه هر دوی این شکل‌ها حالت انیمیشنی مناسبی دارند، ولی همانطور که می‌بینید، شکل‌های رسم‌شده به کمک دستور <code>S</code> (سمت چپ) حالت بسیار بهتری دارند، حتی با وجود اینکه ویژگی <code>lineJoin</code> برای شکل سمت راست به <code>round</code> تغییر کرده، ولی باز هم نسبت به خم‌های سمت چپ کیفیت کمتری دارند.</p>
<p><img decoding="async" src="https://css-tricks.ir/wp-content/uploads/2021/10/canvas-shape-curve.png" alt="canvas shape curve" /></p>
<p>نمونه‌ی بالا شاید کاربرد چندانی نداشته باشد، اما از این جهت بررسی شد تا کاربرد دستورات <code>path</code> را نشان دهد. استفاده از این دستورات به جای متد‌ها می‌تواند در موارد مشابه ما را از دردسر‌های زیادی نجات دهد! اگر هنوز با دستورات <code>path</code> آشنایی ندارید، بهتر است یک حداقل آشنایی با این دستورات پیدا کنید، به‌ویژه به این دلیل که با متد‌های مشابه در canvas سر و کار دارید. در زیر می‌توانید یک نمونه‌ی پیشرفته‌تر از مدل بالا را ببینید:</p>
<p class="codepen" data-height="591" data-default-tab="result" data-slug-hash="abweyKq" data-preview="true" data-user="Hossein_Rafie" style="height: 591px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/Hossein_Rafie/pen/abweyKq"><br />
  Canvas Cells</a> by Hossein Rafie (<a href="https://codepen.io/Hossein_Rafie">@Hossein_Rafie</a>)<br />
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<p><script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script></p>
<h2>متد addPath</h2>
<p>آخرین متد اشیاء کلاس Path2D متد addPath است. این متد به اندازه‌ی خود این کلاس دارای اهمیت است! به کمک این متد می‌توان یک شئ Path2D دیگر را به شکل خود اضافه کنیم. این متد فقط یک ورودی از نوع Path2D دریافت می‌کند. به کد زیر دقت کنید. در کد زیر ابتدا شکل main_path ایجاد شده، و سپس به کمک متد addPath، یک شکل دیگر به آن اضافه می‌شود:</p>
<pre><code class="language-javascript">
let main_path = new Path2D;
main_path.arc(200, 200, 100, 0, Math.PI * 2);

let additional = new Path2D("M 100 100, H 100, V 100, H -100, Z");
main_path.addPath(additional);

ctx.lineWidth = 5;
ctx.stroke(main_path);
</code></pre>
<p>این متد می‌تواند در مواردی که می‌خواهیم چند شکل را همزمان رسم کنیم، یا از دستورات هر دو استاندارد برای رسم شکل استفاده کنیم کاربرد دارد. برای نمونه متد <code>arcTo</code> معادل خاصی در دستورات <code>path</code> ندارد و شاید بخواهیم از آن درون شکل خود استفاده کنیم، به کمک این دستور می‌توانیم به راحتی ترسیمات ترکیبی ایجاد کنیم. بدون وجود این متد، ایجاد چنین ترسیماتی یا بسیار سخت یا غیرممکن است. تنها نکته‌ی باقی‌مانده این است که پشتیبانی مرورگر‌ها از این متد کمی متفاوت است. پس پیش از استفاده از آن نگاهی به <a href="https://caniuse.com/mdn-api_path2d_addpath">جدول پشتیبانی</a> آن بیاندازید.</p>
<h2>بررسی چند نمونه</h2>
<p>در این بخش به بررسی چند نمونه می‌پردازیم. این نمونه‌ها پیچیدگی چندانی ندارند اما ممکن است برای شما تازگی داشته باشند. سعی کنید آن‌ها را به‌خوبی بررسی کرده و روش کار آن‌ها را درک کنید. در ابتدای هر نمونه، توضیحات کوتاهی درباره‌ی اینکه کد چه کاری انجام می‌دهد آورده شده است.</p>
<h3>خم پیوسته با نشانگر</h3>
<p>در این برنامه با هر کلیک کاربر روی عنصر canvas، یک خم پیوسته و بدون شکستگی (از مبدا مرکز عنصر) به مختصات نشانگر رسم می‌شود. همچنین شکل دارای انیمیشن روی خط‌چین است. نمونه‌ی مشابه آن در آموزش‌های قبلی نیز بررسی شده است:</p>
<pre><code class="language-javascript">
cvs.width = cvs.height = 500;

let path = new Path2D,
    str = `M ${cvs.width / 2} ${cvs.height / 2}`,

    dash_array = [40, 20], offset = 0,
    max_offset = dash_array.reduce((a, b) =&gt; a + b);

function random (min, max) {
    return ((Math.random() * (max - min + 1) * 1000 | 0) / 1000) + min;
}

function clear (context) {
    context.save();
    
    context.globalCompositeOperation = "copy";
    context.fillStyle = "rgba(0, 0, 0, 0)";

    context.fillRect(0, 0, 1, 1);
    context.restore();
}

function add_curve (e) {
    let box = cvs.getBoundingClientRect(),

        x1 = e.clientX - box.left,
        y1 = e.clientY - box.top,

        x2 = random(0, cvs.width),
        y2 = random(0, cvs.height);
    
    str += ` S ${x2} ${y2} ${x1} ${y1}`;
    path = new Path2D(str);
}

function draw_path () {
    clear(ctx);

    offset = ++offset % max_offset;
    ctx.lineDashOffset = -offset;

    ctx.stroke(path);
    requestAnimationFrame(draw_path);
}

ctx.setLineDash(dash_array);
ctx.lineCap = ctx.lineWidth = "round";

ctx.lineWidth = 3;
ctx.strokeStyle = "#111";

cvs.addEventListener("click", add_curve);
requestAnimationFrame(draw_path);
</code></pre>
<h3>طرح مارپیچ</h3>
<p>در این برنامه با هر حرکت نشانگر در صفحه، یک مارپیچ به کمک کلاس <code>Spiral</code> ایجاد می‌شود. این کلاس مارپیچ‌هایی ایجاد می‌کند که حرکتشان در یک مختصات تصادفی آغاز شده و به مرور چرخیده و کوچک می‌شوند. روش کار این برنامه شبیه به دایره‌های سرگردان است که در بخش *آموزش انیمیشن* بررسی شد:</p>
<pre><code class="language-javascript">
cvs.width = 1200;
cvs.height = 700;

function random (min, max) {
    return ((Math.random() * (max - min + 1) * 1000 | 0) / 1000) + min;
}

function random_color () {
    return `rgb(${random(0, 255)},
                ${random(0, 255)},
                ${random(0, 255)}`;
}

class Spiral
{
    constructor (x, y) {
        this.x = x + random(-offset, offset);
        this.y = y + random(-offset, offset);

        this.angle = Math.random() * Math.PI * 2;
        this.angle_speed = random(10, 20) / 15;
        
        this.size = random(5, 20);
        this.size_rate = this.size / 30;

        this.speed = this.size;
        this.color = random_color();

        this.status = true;
        this.angle_speed *= ((Math.random() * 2 | 0) === 0) ? 1 : -1;
    }

    draw () {
        if (!this.status) return;
        ctx.save();

        ctx.translate(this.x, this.y);
        ctx.scale(this.size, this.size);

        ctx.fillStyle = this.color;
        ctx.fill(path);

        ctx.restore();
    }

    update () {
        if (this.size === 0) {
            this.status = false;
            return;
        }

        this.x += this.speed * Math.cos(this.angle);
        this.y += this.speed * Math.sin(this.angle);

        this.angle += this.angle_speed;
        this.size = Math.max(0, this.size - this.size_rate);

        this.speed -= 0.1;
    }
}

function loop () {
    ctx.save();

    ctx.fillStyle = "rgba(0, 0, 0, 0.05)";
    ctx.fillRect(0, 0, cvs.width, cvs.height);

    ctx.restore();

    for (let i = spirals.length - 1; i &gt;= 0; i--) {
        if (spirals[i].status === false) {
            spirals.splice(i, 1);
            continue;
        }

        spirals[i].draw();
        spirals[i].update();
    }

    let x = random(0, cvs.width),
        y = random(0, cvs.height);

    if (spirals.length &lt; 30) { spirals.push(new Spiral(x, y)); }

    requestAnimationFrame(loop);
}

let lerp = 0.15,
    offset = 50,
    
    spirals = [],
    path = new Path2D;

path.arc(0, 0, 1, 0, Math.PI * 2);
loop();
</code></pre>
<h3>ترکیب رنگ‌ها</h3>
<p>در این برنامه به کمک کلاس <code>Line</code> خطوطی با رنگ و ضخامت تصادفی ایجاد می‌شوند و در آرایه‌ی <code>lines</code> قرار می‌گیرند. نقطه‌ی اصلی این خطوط در هر بار اجرای متد <code>draw</code> به یک مختصات تصادفی حرکت کرده و خط حاصل از این حرکت رسم می‌شود. با هر کلیک کاربر روی عنصر، تعدادی خط در آن نقطه ایجاد می‌شوند، البته اگر تعداد خطوط به <code>max</code> رسیده باشد، دیگر خطی به برنامه اضافه نمی‌شود. همچنین به کمک عملگر <code>%</code> در انتهای متد <code>draw</code> از خارج شدن خطوط از صفحه جلوگیری می‌کنیم، یعنی اگر خطی از یک طرف عنصر خارج شود، در آن طرف عنصر ظاهر می‌شود:</p>
<pre><code class="language-javascript">
cvs.width = 1200;
cvs.height = 700;

function random (min, max) {
    return ((Math.random() * (max - min + 1) * 1000 | 0) / 1000) + min;
}

function random_color () {
    return `rgb(${random(0, 255)},
                ${random(0, 255)},
                ${random(0, 255)}`;
}

class Line
{
    constructor (x, y) {
        this.x = x;
        this.y = y;

        this.color = random_color();
        this.width = random(1, 3);
    }

    draw () {
        ctx.save();
        ctx.beginPath();

        ctx.moveTo(this.x, this.y);

        this.x += (random(0, 6) - 3.5) * 2;
        this.y += (random(0, 6) - 3.5) * 2;

        ctx.lineTo(this.x, this.y);

        ctx.lineWidth = this.width;
        ctx.strokeStyle = this.color;

        ctx.stroke();
        ctx.restore();

        this.x = (this.x + cvs.width) % cvs.width;
        this.y = (this.y + cvs.height) % cvs.height;
    }
}

let lines = [],
    count = 300,
    max = 400;

function init_lines () {
    for (let i = 0; i &lt; count; i++) {
        lines[i] = new Line(random(0, cvs.width), random(0, cvs.height));
    }
}

function add_line (e) {
    let box = cvs.getBoundingClientRect(),

        x = e.clientX - box.left,
        y = e.clientY - box.top;
    
    for (let i = 0; i &lt; count / 10; i++) {
        if (lines.length &lt; max) lines.push(new Line(x, y)); } } function draw_lines () { lines.forEach((line) =&gt; line.draw());
    requestAnimationFrame(draw_lines);
}

init_lines();
draw_lines();

cvs.addEventListener("click", add_line);
</code></pre>
<p><img decoding="async" src="https://css-tricks.ir/wp-content/uploads/2021/10/canvas-lines.png" alt="canvas colored lines" /></p>
<h3>شبکه‌ی ذرات</h3>
<p>احتمالا مشابه این طرح را در صفحات وب دیده‌اید. در این برنامه نقاطی با سرعت مشخص در صفحه حرکت می‌کنند و اگر بیشتر از یک فاصله‌ای به یکدیگر نزدیک شوند، خطی آن‌ها را به هم متصل می‌کند. می‌توانیم برای خطوط شفافیت نیز تعیین کنیم:</p>
<pre><code class="language-javascript">
cvs.width = 1200;
cvs.height = 700;

function random (min, max) {
    return ((Math.random() * (max - min + 1) * 1000 | 0) / 1000) + min;
}

class Particle
{
    constructor () {
        this.x = random(0, cvs.width);
        this.y = random(0, cvs.height);

        this.speed = [
            random(0, speed * 2) - speed,
            random(0, speed * 2) - speed
        ];
    }

    draw (path) {
        path.moveTo(this.x + radius, this.y);
        path.arc(this.x, this.y, radius, 0, Math.PI * 2);

        this.x += this.speed[0];
        this.y += this.speed[1];

        if (this.x &lt; -radius) this.x = cvs.width + radius;
        if (this.x &gt; cvs.width + radius) this.x = -radius;

        if (this.y &lt; -radius) this.y = cvs.height + radius;
        if (this.y &gt; cvs.height + radius) this.y = -radius;
    }
}

Particle.distance = function (p1, p2) {
    return ((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2) ** 0.5;
}

let background = "#111",
    foreground = "#CC0",

    count = 200,
    particles = [],
    distance = 80,
    
    speed = 1,
    radius = 5;

function init_particles () {
    for (let i = 0; i &lt; count; i++) {
        particles[i] = new Particle;
    }
}

function draw_particles () {
    ctx.fillStyle = background;
    ctx.fillRect(0, 0, cvs.width, cvs.height);
    
    let fill_path = new Path2D,
        stroke_path = new Path2D,

        i, j;
    
    for (i = 0; i &lt; count; i++) {
        particles[i].draw(fill_path);

        for (j = i + 1; j &lt; count; j++) {
            if (Particle.distance(particles[i], particles[j]) &lt; distance) {
                stroke_path.moveTo(particles[i].x, particles[i].y);
                stroke_path.lineTo(particles[j].x, particles[j].y);
            }
        }
    }

    ctx.fillStyle = foreground;

    ctx.stroke(stroke_path);
    ctx.fill(fill_path);

    requestAnimationFrame(draw_particles)
}

ctx.lineWidth = 3;
ctx.strokeStyle = foreground;

init_particles();
draw_particles();
</code></pre>
<p><img decoding="async" src="https://css-tricks.ir/wp-content/uploads/2021/10/canvas-particles.png" alt="canvas particles" /></p>
<p>همچنین در زیر می‌توانید یک نمونه‌ی پیشرفته‌تر از همین برنامه را ببینید. این برنامه هرچند پیچیده به نظر می‌رسد، ولی پایه و اساس آن بررسی فاصله‌ی میان نقاط است، یعنی تفاوت آنچنانی با کد بالا ندارد:</p>
<p class="codepen" data-height="520" data-default-tab="result" data-slug-hash="BaZeRrY" data-preview="true" data-user="Hossein_Rafie" style="height: 520px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/Hossein_Rafie/pen/BaZeRrY"><br />
  Canvas Particles</a> by Hossein Rafie (<a href="https://codepen.io/Hossein_Rafie">@Hossein_Rafie</a>)<br />
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<p><script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script></p>
<h2>نتیجه‌گیری</h2>
<p>در این آموزش سعی کردیم تا جای ممکن ویژگی‌های کلاس <code>Path2D</code> و توانایی‌هایی که به canvas اضافه می‌کند را همراه با چندین نمونه بررسی کنیم. خوشبختانه در طول این آموزش‌ها، دانش شما از این فناوری به جایی رسیده که بتوانید به سادگی طرح‌های گرافیکی پیچیده‌ای ایجاد کنید. در آموزش بعدی، آخرین قدم در canvas یعنی تعامل آن با CSS و تعیین اندازه و دیگر موارد مهم بررسی خواهند شد.</p>
<p>نوشته <a rel="nofollow" href="https://css-tricks.ir/tutorial/%da%a9%d9%84%d8%a7%d8%b3-path2d-%d9%82%d8%af%d8%b1%d8%aa-%d8%ac%d8%af%db%8c%d8%af-canvas/">کلاس Path2D قدرت جدید canvas</a> اولین بار در <a rel="nofollow" href="https://css-tricks.ir">آموزش طراحی وب</a> پدیدار شد.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.ir/tutorial/%da%a9%d9%84%d8%a7%d8%b3-path2d-%d9%82%d8%af%d8%b1%d8%aa-%d8%ac%d8%af%db%8c%d8%af-canvas/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>کلاس ImageData در canvas</title>
		<link>https://css-tricks.ir/tutorial/%da%a9%d9%84%d8%a7%d8%b3-imagedata-%d8%af%d8%b1-canvas/</link>
					<comments>https://css-tricks.ir/tutorial/%da%a9%d9%84%d8%a7%d8%b3-imagedata-%d8%af%d8%b1-canvas/#respond</comments>
		
		<dc:creator><![CDATA[حسین رفیعی]]></dc:creator>
		<pubDate>Sat, 13 Nov 2021 05:02:52 +0000</pubDate>
				<category><![CDATA[آموزش]]></category>
		<category><![CDATA[canvas]]></category>
		<category><![CDATA[effect]]></category>
		<category><![CDATA[image]]></category>
		<guid isPermaLink="false">https://css-tricks.ir/?p=9449</guid>

					<description><![CDATA[<p>در این آموزش به بررسی کلاس ImageData، متد‌های مربوط به آن، و کاربرد‌های آن، به‌ویژه ایجاد جلوه‌های تصویری می‌پردازیم.</p>
<p>نوشته <a rel="nofollow" href="https://css-tricks.ir/tutorial/%da%a9%d9%84%d8%a7%d8%b3-imagedata-%d8%af%d8%b1-canvas/">کلاس ImageData در canvas</a> اولین بار در <a rel="nofollow" href="https://css-tricks.ir">آموزش طراحی وب</a> پدیدار شد.</p>
]]></description>
										<content:encoded><![CDATA[<p>فناوری canvas در کنار تمام امکاناتی که دارد، امکان دسترسی مستقیم به اطلاعات تصویری را نیز به ما می‌دهد. کلاس ImageData این امکان را به ما می‌دهد که اطلاعات تصویری را از canvas خارج کرده و برای اهداف مختلف از آن‌ها استفاده کنیم. در ادامه به کاربرد و روش استفاده از این کلاس می‌پردازیم.</p>
<h2>کارکرد این کلاس چیست؟</h2>
<p>این کلاس اطلاعات درون canvas را به صورت یک آرایه از اعداد بین 0 تا 255 به ما می‌دهد. این اطلاعات، مقدار رنگ‌های هر پیکسل درون canvas هستند. با داشتن این آرایه، می‌توانیم یک تصویر را به صورت متن دخیره کنیم یا برای کار خاصی به سرور بفرستیم، یا اینکه پردازشات خاصی مانند ایجاد جلوه روی آن‌ها انجام دهیم.</p>
<p class="oi-content oi-content--warning">در هنگام کار با متد‌های مربوط به این کلاس ممکن است خطای <code>cross-origin</code> دریافت کنید. این خطا هنگامی رخ می‌دهد که مثلا یک تصویر در canvas رسم شده باشد و سعی شود با این کلاس آن را به صورت اطلاعات عددی درآورد. این خطای امنیتی اجازه نمی‌دهد تصاویر کاربر به این روش دزدیده شوند. برای رفع این خطا می‌توانید تصاویر را به <code>base64</code> تبدیل کرده و مستقیما درون کد از آن‌ها استفاده کنید. یا اینکه می‌توانید از یک سرور مجازی استفاده کنید.</p>
<h2>متد createImageData</h2>
<p>این متد یک شئ <code>ImageData</code> در ابعاد <code>width</code> و <code>height</code> ایجاد می‌کند. اگر یک شئ <code>ImageData</code> دیگر به عنوان ورودی این متد داده شود، یک شئ خالی با ابعاد آن شئ ساخته می‌شود:</p>
<pre><code class="language-javascript">
let new_data = ctx.createImageData(width, height);
let new_data = ctx.createImageData(another_image_data);
</code></pre>
<h2>متد getImageData</h2>
<p>این متد اطلاعات یک بخش از canvas به مختصات <code>(x,y)</code> و در ابعاد <code>width</code> و <code>height</code> را به یک شئ <code>ImageData</code> تبدیل کرده و بازمی‌گرداند. نمای کلی این متد به صورت زیر است:</p>
<pre><code class="language-javascript">
let my_image = ctx.createImageData(x, y, width, height);
</code></pre>
<h2>متد putImageData</h2>
<p>این متد اطلاعات یک شئ <code>ImageData</code> را در یک بخش از canvas به مختصات <code>(x,y)</code> قرار می‌دهد. نکته‌ی مهمی که باید به آن توجه کنید این است که این متد اطلاعات را شبیه به متد <code>drawImage</code> قرار نمی‌دهد، یعنی اگر یک شئ <code>ImageData</code> شامل یک تصویر کاملا شفاف درون canvas قرار دهید، بخش مورد نظر کاملا شفاف می‌شود! در حالی که اگر همان تصویر با متد <code>drawImage</code> رسم شود تغییری در canvas ایجاد نمی‌شود. این متد به صورت زیر است:</p>
<pre><code class="language-javascript">
ctx.putImageData(my_image, x, y);
</code></pre>
<h2>ویژگی‌های شئ ImageData</h2>
<p>ممکن است این پرسش برایتان پیش آمده باشد که چرا در متد <code>putImageData</code> نمی‌توان طول و عرض را تعیین کرد؟ این مورد به ویژگی‌های این شئ مربوط می‌شود که باید پیش از استفاده، آن‌ها را به یاد داشته باشید.</p>
<ul>
<li>ماتریس تبدیلات روی این متد‌ها اثر نمی‌گذارد. مهم نیست چه نوع تبدیلاتی روی canvas اجرا کرده باشید، متد‌های <code>getImageData</code> و <code>putImageData</code> هیچ تاثیری از آن‌ها نمی‌پذیرند.</li>
<li>تغییر اندازه‌ی این نوع شئ امکان‌پذیر نیست. شئ <code>ImageData</code> هنگام ساخته شدن ویژگی‌های <code>width</code> و <code>height</code> را درون خود دارد و نمی‌توان اندازه‌ی آن را تغییر داد. تفاوت اصلی این شئ با تصویر نیز همین است. ابعاد این شئ ثابت هستند.</li>
</ul>
<h2>ساختار یک شئ ImageData</h2>
<p>این شئ سه ویژگی دارد. ویژگی‌های <code>width</code> و <code>height</code> که پیش از این به آن‌ها اشاره شد و ابعاد تصویر درون شئ را در خود دارند. ویژگی سوم، و مهمترین ویژگی این شئ، ویژگی <code>data</code> است که یک آرایه به طول <code>width * height * 4</code> است. این آرایه اطلاعات تصویر را دارد. درون این آرایه، اطلاعات هر پیکسل به ترتیب قرمز، سبز، آبی، و آلفا ذخیره شده‌اند.</p>
<p>شاید برایتان این پرسش پیش بیاید که یک آرایه‌ی یک‌بعدی چطور یک آرایه‌ی دو‌بعدی از تصاویر را در خود دارد؟ ترتیب این پیکسل‌ها به چه صورت است؟ این آرایه از گوشه‌ی چپ و بالای تصویر آغاز شده و به سمت راست حرکت می‌کند، وقتی به طول تصویر یا <code>width</code> رسید، یک ردیف پایین می‌آید. به این ترتیب یک تصویر کامل درون یک آرایه‌ی یک‌بعدی قرار می‌گیرد. در نمونه‌های بعدی لازم است که مختصات یک پیکسل را درون این آرایه به دست آوریم که برای آن روش‌هایی وجود دارد و به آن‌ها خواهیم پرداخت.</p>
<h2>ایجاد جلوه‌های تصویری</h2>
<p>حال می‌خواهیم از این کلاس برای ایجاد جلوه‌های تصویری استفاده کنیم. از آنجایی که نمی‌توان یک تصویر را مستقیما تبدیل به شئ <code>ImageData</code> کرد، ابتدا تصویر خود را در یک canvas رسم کرده و با کمک متد <code>getImageData</code> آن را تبدیل می‌کنیم. توجه کنید که طول و عرض تصویر بهتر است بزرگ‌تر از 500 نباشد. علت آن را در انتهای آموزش خواهید فهمید. همچنین مراقب باشید که هنگام کار خطای <code>cross-origin</code> دریافت نکنید.</p>
<p>کد اولیه‌ی ما به صورت زیر است. در برنامه‌ی زیر یک تصویر به طول و عرض 500 در سمت چپ عنصر <code>cvs</code> رسم شده و اطلاعات آن به صورت یک شئ <code>ImageData</code> دریافت می‌شود، سپس اطلاعات این شئ توسط تابع <code>change_img_data</code> تغییر کرده و سپس در نیمه‌ی سمت راست عنصر <code>cvs</code> چاپ می‌شود. در ادامه کد‌های لازم را درون این برنامه قرار دهید تا اجرا شوند:</p>
<pre><code class="language-javascript">
let cvs = document.getElementById("cvs"),
    ctx = cvs.getContext("2d");

cvs.width = 1000;
cvs.height = 500;

let img = new Image,
    img_data;

img.onload = draw_image;
img.src = "flower.jpg";

function draw_image () {
    ctx.drawImage(img, 0, 0, 500, 500);
    img_data = ctx.getImageData(0, 0, 500, 500);

    change_img_data();
    ctx.putImageData(img_data, 500, 0);
}

function change_img_data () {
    let i, l;
    
    for (i = 0, l = img_data.width * img_data.height * 4; i &lt; l; i += 4) {
        img_data.data[i + 0] /* R */
        img_data.data[i + 1] /* G */
        img_data.data[i + 2] /* B */
        img_data.data[i + 3] /* A */
    }
}
</code></pre>
<p>ابتدا باید به روش کار تابع <code>change_img_data</code> بپردازیم؛ کارکرد دیگر بخش‌های کد نیاز به توضیح خاصی ندارد. درون این تابع یک حلقه به طول آرایه‌ی <code>img_data.data</code> و با گام 4 ایجاد می‌شود. این حلقه به تمام پیکسل‌های شئ <code>img_data</code> دسترسی دارد و می‌تواند آن‌ها را تغییر دهد. همانطور که درون حلقه مشخص شده، می‌توان به هرکدام از رنگ‌های هر پیکسل دسترسی پیدا کرد و آن‌ها را تغییر داد.</p>
<p>در ادامه قرار است برای هر جلوه‌ی تصویری، یک تابع تعریف کنیم. پس بهتر است این تابع را باز هم تغییر دهیم تا بتوانیم از توابع تعریف‌شده بهتر و راحت‌تر استفاده کنیم. از آنجایی که کانال آلفا (شفافیت) در این جلوه‌ها تغییر نمی‌کند، از آن چشم‌پوشی می‌کنیم و فقط به رنگ‌ها می‌پردازیم. تابع <code>change_img_data</code> را به صورت زیر اصلاح کنید. در بخش مشخص‌شده با توضیحات، توابع موردنظر خود را قرار خواهیم داد:</p>
<pre><code class="language-javascript">
function change_img_data () {
    let i, l, r, g, b;
    
    for (i = 0, l = img_data.width * img_data.height * 4; i &lt; l; i += 4) {
        r = img_data.data[i + 0];
        g = img_data.data[i + 1];
        b = img_data.data[i + 2];
        
        /* --- */
        [r, g, b] = the_effect(r, g, b);
        /* --- */

        img_data.data[i + 0] = r;
        img_data.data[i + 1] = g;
        img_data.data[i + 2] = b;
    }
}
</code></pre>
<p>در کد بالا به جای <code>the_effect</code> نام توابعی که خواهیم نوشت را قرار دهید. البته فراموش نکنید که خود تابع را نیز به کد اضافه کنید. در ادامه برای بررسی توابع پیشرفته‌تر لازم خواهد بود که باز هم تابع <code>change_img_data</code> تغییر کند اما فعلا به این شکل آن را به کار ببرید.</p>
<p>طبق کد بالا، تابع <code>the_effect</code>، مستقل از اینکه چه نامی دارد، حداقل سه ورودی خواهد داشت و یک آرایه با سه عضو نیز به عنوان خروجی خواهد داشت که همان رنگ‌های پیکسل هستند که تغییرات روی آن‌ها اعمال شده است. همچنین خروجی‌های این تابع باید اعدادی صحیح بین 0 و 255 باشند.</p>
<h3>جلوه‌ی سیاه و سفید (grayscale)</h3>
<p>روش اصلی برای ایجاد این جلوه، استفاده از میانگین سه رنگ در یک پیکسل، به عنوان مقدار هر سه رنگ در آن پیکسل است. در کد زیر تابع <code>grayscale</code> میانگین رنگ سه مقدار ورودی را حساب کرده و آن را در آرایه‌ی خروجی قرار می‌دهد. کد زیر را به کد اصلی اضافه کرده و نتیجه را ببینید:</p>
<pre><code class="language-javascript">
function grayscale (r, g, b) {
    let avg = (r + g + b) / 3 | 0;
    return [avg, avg, avg];
}
</code></pre>
<p><img decoding="async" src="https://css-tricks.ir/wp-content/uploads/2021/09/canvas-effect-grayscale.png" alt="canvas image grayscale" /></p>
<h3>سیاه و سفید جهت‌دار (vector grayscale)</h3>
<p>می‌توان ایده‌ی بالا را گسترش داد و به جای گرفتن میانگین ساده‌ی رنگ‌ها، یک میانگین وزن‌دار از آن‌ها گرفت، به گونه‌ای که تاثیر برخی رنگ‌ها در این میانگین بیشتر یا کمتر باشد. نام «جهت‌دار» به این دلیل انتخاب شده که این میانگین می‌تواند شبیه به یک بردار سه‌بعدی در فضای <code>rgb</code> دیده شود! این تابع شش ورودی می‌پذیرد که سه ورودی دوم ضرایب میانگین (یا همان جهت بردار!) هستند. درون حلقه این میانگین را تغییر دهید تا بهتر متوجه کارکرد این جلوه شوید:</p>
<pre><code class="language-javascript">
function vector_grayscale (r, g, b, x, y, z) {
    let c = (r * x + g * y + b * z) / (x + y + z) | 0;
    return [c, c, c];
}
</code></pre>
<p><img decoding="async" src="https://css-tricks.ir/wp-content/uploads/2021/09/canvas-effect-grayscale-vector.png" alt="canvas image vector grayscale" /></p>
<p>می‌توانید بیرون از حلقه سه عدد تصادفی ایجاد کرده و درون حلقه به جای سه ورودی دوم از آن‌ها استفاده کنید تا هر بار یک جلوه‌ی سیاه و سفید متفاوت روی تصویر اعمال شود. در کد زیر به جای ضرایب میانگین از اعداد تصادفی استفاده شده است:</p>
<pre><code class="language-javascript">
let x = Math.random(),
    y = Math.random(),
    z = Math.random();
    
/* for (...) */

[r, g, b] = vector_grayscale(r, g, b, x, y, z);

/* ... */
</code></pre>
<h3>جلوه‌ی نگاتیو (negtive)</h3>
<p>احتمالا مشابه این جلوه را در عکاسی دیده باشید. کد لازم برای ساخت این جلوه بسیار ساده و به صورت زیر است. کافیست تفاوت هر رنگ را از 255 (که رنگ سفید است) حساب کرده و به جای رنگ اصلی بگذاریم:</p>
<pre><code class="language-javascript">
function negative (r, g, b) {
    return [255 - r, 255 - g, 255 - b];
}
</code></pre>
<p><img decoding="async" src="https://css-tricks.ir/wp-content/uploads/2021/09/canvas-effect-negative.png" alt="canvas image negative" /></p>
<h3>جلوه‌ی جا‌به‌جایی رنگ (color swap)</h3>
<p>این جلوه جای مقدار‌های رنگ را با یکدیگر عوض می‌کند. می‌توانیم به جای نوشتن یک تابع طولانی، کمی از خلاقیت استفاده کنیم و بدون نوشتن یک تابع، درون حلقه جای مقدار‌های رنگ را عوض کنیم. به کد زیر دقت کنید. در کد زیر جای رنگ‌های قرمز و آبی هر پیکسل عوض می‌شود. می‌توانید هر شش ترکیب مختلف را امتحان کنید:</p>
<pre><code class="language-javascript">
[r, g, b] = [b, g, r];
</code></pre>
<p><img decoding="async" src="https://css-tricks.ir/wp-content/uploads/2021/09/canvas-effect-color-swap.png" alt="canvas image color swap" /></p>
<h3>جلوه‌ی روشنایی (brightness)</h3>
<p>این جلوه باعث روشن‌تر شدن تصویر می‌شود. تابع مربوط به این جلوه چهار ورودی می‌پذیرد که سه ورودی اول رنگ‌ها هستند و ورودی چهارم مقدار روشنایی است. کار این تابع ساده است و هر مقدار رنگ را با مقدار روشنایی جمع می‌کند، البته از آنجایی که ورودی چهارم می‌تواند منفی هم باشد، به کمک توابع <code>min</code> و <code>max</code> اطمینان حاصل می‌کند که مقدار رنگ در محدوده‌ی 0 و 255 باقی می‌ماند:</p>
<pre><code class="language-javascript">
function brightness (r, g, b, s) {
    s = s | 0;
    
    r = Math.max(0, Math.min(255, r + s));
    g = Math.max(0, Math.min(255, g + s));
    b = Math.max(0, Math.min(255, b + s));

    return [r, g, b];
}
</code></pre>
<h3>جلوه‌ی سیاه و سفید پله‌ای (quantize)</h3>
<p>این جلوه که نسخه‌ای پیشرفته‌تر از جلوه‌ی سیاه و سفید است، نه تنها تصویر را سیاه و سفید می‌کند، بلکه رنگ‌های آن را نیز محدود می‌کند. کد آن ممکن است کمی پیچیده به نظر برسد ولی روش کار آن بسیار ساده است. این تابع علاوه بر سه ورودی که همه‌ی توابع دارند، یک ورودی دیگر به نام <code>n</code> نیز دارد که تعداد طیف‌های خاکستری را مشخص می‌کند.</p>
<p>در خط اول، تابع بخش اعشاری <code>n</code> را حذف کرده و به کمک متد <code>max</code> اطمینان حاصل می‌کند که این عدد از 2 کوچکتر نباشد، زیرا نمی‌توان کمتر از دو طیف در یک تصویر داشت. دو عملگر <code>~~</code> شبیه به عملگر <code>| 0</code> رفتار کرده و قسمت اعشاری را حذف می‌کنند. در ادامه میانگین سه رنگ محاسبه شده و در متغیر <code>avg</code> ذخیره می‌شود. این میانگین بین 0 و 255 نیست، بلکه بین 0 و 1 است.</p>
<p>در ادامه حاصل ضرب این مقدار در <code>n+1</code> حساب شده، بخش صحیح آن تقسیم بر <code>n-1</code> شده و کل عبارت در 255 ضرب می‌شود. این مقدار باید عددی بین 0 و 255 باشد که بنا به مقدار <code>avg</code> روی یکی از <code>n</code> پله‌ی بین 0 تا 255 قرار می‌گیرد. در نهایت قسمت صحیح این عدد (همراه با کنترل نهایی) اعمال شده و به عنوان خروجی قرار می‌گیرد:</p>
<pre><code class="language-javascript">
function quantize (r, g, b, n) {
    n = Math.max(~~n, 2);

    let avg = (r + g + b) / (3 * 255),
        c = ~~(avg * (n + 1)) / (n - 1) * 255;
    
    c = Math.max(0, Math.min(255, ~~c));
    return [c, c, c];
}
</code></pre>
<p><img decoding="async" src="https://css-tricks.ir/wp-content/uploads/2021/09/canvas-effect-quantize.png" alt="canvas image quantize" /></p>
<h2>جلوه‌های پیچیده‌تر</h2>
<p>تاکنون تمام جلوه‌هایی که بررسی شدند فقط روی یک پیکسل متمرکز بودند. حال می‌خواهیم جلوه‌هایی را بررسی کنیم که نه تنها پیکسل فعلی، بلکه پیکسل‌های همسایه‌ی آن را نیز درگیر می‌کنند. برای کار با این جلوه‌ها لازم است که مختصات هر پیکسل را بدانید؛ بنابراین باید با دو تابع خاص آشنا شوید. در زیر دو تابع <code>c2i</code> و <code>i2c</code> نوشته شده‌اند. کار این توابع تبدیل مختصات به اندیس آرایه و بالعکس است. حرف c مخفف coordinates یا مختصات و حرف i نیز مخفف index یا اندیس است.</p>
<pre><code class="language-javascript">
let c2i = (x, y, w) =&gt; (w * y + x) * 4,
    i2c = (i, w) =&gt; [((i / 4) % w), ~~(i / (w * 4))];
</code></pre>
<p>علاوه بر دو تابع بالا، لازم است با تابع جدیدی به نام <code>convolute</code> آشنا شویم. این تابع ویژه یک شئ <code>ImageData</code> و یک آرایه با 9 عضو به عنوان ماتریس ضرایب می‌پذیرد. شاید کد این تابع به نظر پیچیده برسد ولی تنها کاری که انجام می‌دهد، این است که مقدار رنگ پیکسل‌های همسایه را در ضریب درون ماتریس ضرب کرده و میانگین نتیجه‌ی نهایی را در پیکسل فعلی قرار می‌دهد. مقدار بازگشتی این تابع یک شئ <code>ImageData</code> است که نتیجه‌ی کار است:</p>
<pre><code class="language-javascript">
function convolute (image_data, m) {
    let destination = ctx.createImageData(image_data),
        
        d = image_data.data,
        e = destination.data,

        w = image_data.width,
        h = image_data.height,

        x, y, i, j, c, t, n,
        l = m.reduce((a, b) =&gt; a + b),
        
        coords = [
            -1, -1, 0, -1, 1, -1,
            -1,  0, 0,  0, 1,  0,
            -1,  1, 0,  1, 1,  1
        ];
    
    for (x = 0; x &lt; w; x++) {
        for (y = 0; y &lt; h; y++) {
            n = c2i(x, y, w);

            for (i = 0; i &lt; 3; i++) {
                t = 0;

                for (j = 0; j &lt; 18; j += 2) {
                    c = d[c2i(x + coords[j], y + coords[j + 1], w) + i];
                    t += c * m[j / 2];
                }

                e[n + i] = Math.max(0, Math.min(255, t / l));
            }

            e[n + 3] = d[n + 3];
        }
    }

    return destination;
}
</code></pre>
<p>در کد تابع، به جای اینکه یک حلقه به اندازه‌ی طول آرایه‌ی اطلاعات ایجاد شود، دو حلقه درون هم به اندازه‌ی طول آرایه ایجاد شده و مقدار‌های <code>x</code> و <code>y</code> هرکدام جداگانه حساب می‌شوند. سپس این دو مقدار به کمک تابع c2i تبدیل به اندیس آرایه می‌شوند. درون این دو حلقه، دو حلقه‌ی کوچک دیگر وجود دارند که مقدار‌های رنگ پیکسل فعلی را با ترکیب رنگ از پیکسل‌های همسایه تعیین می‌کنند.</p>
<p>ماتریس ضریبی که ورودی دوم این تابع است، تعیین می‌کند که چه جلوه‌ای روی تصویر اعمال شود. در زیر به دو جلوه‌ی مهم می‌پردازیم. کاربرد این تابع بسیار بیشتر است و می‌تواند جلوه‌های ویژه‌ای ایجاد کند، اما برای حفظ سادگی مطلب از بازگو کردن آن‌ها می‌پرهیزیم.</p>
<h3>جلوه‌ی محو شدن (blur)</h3>
<p>این جلوه تصویر را تار و به نوعی محو می‌کند. این جلوه هنگامی ایجاد می‌شود که پیکسل‌های همسایه به یک اندازه روی هر پیکسل اثر بگذارند، یعنی تمام درایه‌های ماتریس ضرایب 1 باشد. کد زیر ساختار این تابع را نشان می‌دهد:</p>
<pre><code class="language-javascript">
function blur (image_data) {
    let matrix = [1, 1, 1,
                  1, 1, 1,
                  1, 1, 1];

    return convolute(image_data, matrix);
}
</code></pre>
<p><img decoding="async" src="https://css-tricks.ir/wp-content/uploads/2021/09/canvas-effect-blur.png" alt="canvas image blur" /></p>
<h3>جلوه‌ی تند کردن (sharpen)</h3>
<p>این جلوه به نوعی برعکس جلوه‌ی بالا عمل کرده و کیفیتی کاذب به تصویر می‌دهد. این جلوه هنگامی ایجاد می‌شود که پیکسل‌های همسایه تاثیر منفی روی پیکسل مرکزی داشته باشند. ساختار تابع این جلوه به صورت زیر است:</p>
<pre><code class="language-javascript">
function sharpen (image_data) {
    let matrix = [ 0, -1,  0,
                  -1,  5, -1,
                   0, -1,  0];

    return convolute(image_data, matrix);
}
</code></pre>
<p><img decoding="async" src="https://css-tricks.ir/wp-content/uploads/2021/09/canvas-effect-sharpen.png" alt="canvas image sharpen" /></p>
<h3>ترکیب چند جلوه</h3>
<p>یک مزیت شئ <code>ImageData</code> این است که می‌توان اطلاعات درون آن را چندین بار تغییر داد و این یعنی می‌توان چندین جلوه‌ی مختلف روی یک تصویر اعمال کرد. برای نمونه می‌توانید ابتدا جلوه‌ی <code>blur</code> را پنج بار اجرا کرده و روی نتیجه، جلوه‌ی <code>quantize</code> را اجرا کنید.</p>
<p><img decoding="async" src="https://css-tricks.ir/wp-content/uploads/2021/09/canvas-effect-complex.png" alt="canvas image complex effects" /></p>
<h2>سرعت پایین</h2>
<p>با وجود اینکه سرعت متد‌های <code>getImageData</code> و <code>putImageData</code> نسبتا بالاست، اما به طور کلی اعمال تغییرات روی یک شئ <code>ImageData</code> سرعت پایینی دارد. زیرا برای اعمال تغییر روی آن لازم است یک حلقه‌ی بزرگ ایجاد شود. جدا از اینکه چه پردازشاتی درون حلقه انجام می‌شود، پردازنده‌ی مرکزی (CPU) مجبور است درگیر آن شود و از آنجایی که پردازنده‌ی مرکزی فقط می‌تواند به تعداد کمی پردازش در لحظه رسیدگی کند، زمان زیادی صرف انجام دستورات حلقه می‌کند.</p>
<p>کافیست یک تصویر در ابعاد 2000 در 1000 به برنامه بدهید تا متوجه این موضوع شوید. هنگام اجرای برنامه، پردازنده با یک حلقه به طول 8000000 مواجه می‌شود و زمان نسبتا زیادی را صرف آن می‌کند و این باعث تاخیر برنامه می‌شود. مشکل از پردازنده‌ی مرکزی نیست، مشکل این است که ما از آن به اشتباه استفاده می‌کنیم! پردازنده‌ی مناسب برای انجام چنین کاری، پردازنده‌ی گرافیکی (GPU) است که می‌تواند پردازشات کوچک بسیاری را در یک لحظه انجام دهد.</p>
<p>این بدین معنی نیست که از اعمال جلوه‌های تصویری با کلاس <code>ImageData</code> خودداری کنید، بلکه فقط باید هنگام کار با آن مراقب باشید، زیرا پردازنده‌ی مرکزی برای انجام چنین کار‌هایی بهینه نیست. می‌توانید از تابع <code>rAF</code> به گونه‌ای استفاده کنید که بتوان گام‌های حلقه را در طول مثلا 5 ثانیه تقسیم‌بندی کرد و از فشار روی پردازنده جلوگیری کرد! راه‌های زیادی برای این کار وجود دارد که بنا بر رویکرد پروژه می‌تواند تغییر کند.</p>
<h2>روش‌های جایگزین</h2>
<p>حال که می‌دانیم تغییر دادن این نوع اشیاء همیشه مناسب نیست، بهتر است به دنبال روش‌های جایگزین باشیم. دو جایگزین بسیار مناسب برای این کار وجود دارند که هم سرعت بسیار بالایی دارند، و هم کار با آن‌ها نسبتا ساده‌است. پیش از این با یکی از این جایگزین‌ها آشنا شده‌ایم.</p>
<h3>ویژگی globalCompositeOperation</h3>
<p>اگر به یاد داشته باشید، حتی هنگام معرفی این ویژگی نیز چند جلوه با آن ایجاد کردیم. اگر بتوانید مقدار‌های مختلف این ویژگی را به خوبی بشناسید و درست از هرکدام استفاده کنید، می‌توانید جلوه‌های زیبای بسیاری ایجاد کنید که با سرعت بالا و کد نسبتا کمتری ایجاد می‌شوند. احتمالا در یکی از آموزش‌های آینده روش‌های ساخت جلوه‌های مختلف با این ویژگی را بررسی خواهیم کرد.</p>
<p>هرچند این ویژگی می‌تواند در بسیاری از موارد کاربردی باشد، اما نمی‌تواند جلوه‌های پیچیده ایجاد کند. برای نمونه، ایجاد جلوه‌ی محو شدن یا تند کردن یا هر جلوه‌ی پیچیده‌ای که از ترکیب رنگ پیکسل‌های همسایه ایجاد می‌شود، با این ویژگی تقریبا غیر ممکن است. همچنین ایجاد جلوه‌های دیگر مانند سیاه و سفید پله‌ای بدون کمک کلاس <code>ImageData</code> نمی‌تواند انجام شود.</p>
<h3>استفاده از WebGL</h3>
<p>شاید انتظار این گزینه را نداشتید، اما WebGL می‌تواند پیچیده‌ترین جلوه‌های تصویری را با بالاترین سرعت ایجاد کند، هرچند استفاده از آن پیچیدگی‌های خود را دارد، اما نتیجه‌ی کار تضمین‌شده است و مستقل از ابعاد تصویر یا پیچیدگی جلوه، سرعت کار بالا خواهد بود و حتی می‌توان روی جلوه‌ها انیمیشن اجرا کرد و با بالاترین سرعت آن را اجرا کرد!</p>
<p>این گزینه برای جلوه‌های تصویری بسیار مناسب است اما آن را پیشنهاد نمی‌کنیم، زیرا یادگیری آن بسیار زمان‌بر و سخت بوده و منابع مناسبی برای آموزش آن (به ویژه منبع فارسی) وجود ندارد.</p>
<h3>استفاده از filter در SVG</h3>
<p>این گزینه شاید بهترین گزینه باشد. فیلتر‌های SVG هم از نظر سادگی و هم از نظر گوناگونی بهترین گزینه برای کار هستند. به کمک این فیلتر‌ها می‌توان جلوه‌های بسیار پیچیده‌ای را با بالاترین سرعت ایجاد کرد. تنها مشکل یادگیری موارد گوناگونی است که این عنصر در اختیار ما قرار می‌دهد. احتمالا در آموزش‌های آینده به این عنصر بیشتر خواهیم پرداخت.</p>
<h2>دیگر کاربرد‌های این کلاس</h2>
<p>این کلاس فقط به ایجاد جلوه‌های تصویری محدود نیست بلکه کاربرد‌های آن فراتر از آن است. این کلاس می‌تواند تصاویر را به آرایه‌های اطلاعاتی تبدیل کند و این یعنی می‌توان تصاویر ایجاد‌شده در canvas را برای دیگران (یا سرور) ارسال کرد، و تصاویر را بدون نیاز به وجود عنصر تصویری ذخیره کرد.  همانطور که متوجه شده‌اید، اگر کاربرد‌های این کلاس فقط به ایجاد جلوه‌های تصویری محدود بود، هیچگاه خطای <code>cross-origin</code> ایجاد نمی‌شد!</p>
<h3>پاک کردن لایه‌ی ترسیمات</h3>
<p>یک روش جالب دیگر برای پاک کردن لایه‌ی ترسیمات، استفاده از یک شئ <code>ImageData</code> خالی به اندازه‌ی کل عنصر canvas است. از آنجایی که متد <code>putImageData</code> از تبدیلات فعلی تاثیر نمی‌پذیرد، می‌توان به سادگی و بدون هیچ مشکلی از کد زیر برای پاک کردن لایه‌ی ترسیمات استفاده کرد:</p>
<pre><code class="language-javascript">
function clear (context) {
    context.putImageData(context.createImageData(context.canvas.width, context.canvas.height), 0, 0);
}
</code></pre>
<p>حتی می‌توانیم برای سرعت بیشتر کار فقط یک شئ <code>ImageData</code> خالی ایجاد کرده و از آن برای پاک کردن استفاده کنیم؛ به این ترتیب دیگر لازم نیست با هر فراخوانی تابع <code>clear</code> یک شئ تازه ایجاد شود. به کمک این تابع جدید نه نیازی به تغییر تبدیلات هست، نه نیازی به تغییر رنگ یا ویژگی <code>globalCompositeOperation</code> و سرعت آن نیز بسیار بالاست.</p>
<p>تنها مشکل باقی‌مانده این است که اگر از متد <code>clip</code> استفاده شده باشد، فقط بخش‌های درون محدوده‌ی ترسیمات پاک خواهند شد. البته این مورد در تمام روش‌هایی که تاکنون بررسی کرده‌ایم نیز وجود دارد و راه‌حل آن این است که در استفاده از متد <code>clip</code> دقت کافی داشته باشید و همراه با آن از <code>save</code> و <code>restore</code> استفاده کنید.</p>
<p>البته توجه کنید، موقعیت‌هایی پیش می‌آید که در آن‌ها لازم است تبدیلات فعلی روی مساحت پاک‌شونده نیز اثر بگذارد. در این موارد استفاده از <code>clearRect</code> بهتر است، اما در مواردی که می‌خواهید کل لایه‌ی ترسیمات را پاک کنید، استفاده از تابع بالا بهتر است؛ همه‌چیز به برنامه‌ی نوشته‌شده بستگی دارد.</p>
<h2>نتیجه‌گیری</h2>
<p>در این بخش سعی کردیم ساختار یک شئ <code>ImageData</code> و رفتار آن را درک کرده و همچنین روش‌های ساخت چندین جلوه‌ی تصویری پرکاربرد را نیز بررسی کردیم. سعی کنید جلوه‌های ویژه‌ی خود را درون حلقه یا به کمک تابع <code>convolute</code> ایجاد کنید یا سعی کنید جلوه‌هایی که برای تصویر بالای پست استفاده شده را بازسازی کنید! در آموزش بعدی به کلاس <code>Path2D</code> و کاربردهای آن می‌پردازیم.</p>
<p>نوشته <a rel="nofollow" href="https://css-tricks.ir/tutorial/%da%a9%d9%84%d8%a7%d8%b3-imagedata-%d8%af%d8%b1-canvas/">کلاس ImageData در canvas</a> اولین بار در <a rel="nofollow" href="https://css-tricks.ir">آموزش طراحی وب</a> پدیدار شد.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.ir/tutorial/%da%a9%d9%84%d8%a7%d8%b3-imagedata-%d8%af%d8%b1-canvas/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>انیمیشن در canvas</title>
		<link>https://css-tricks.ir/tutorial/%d8%a7%d9%86%db%8c%d9%85%db%8c%d8%b4%d9%86-%d8%af%d8%b1-canvas/</link>
					<comments>https://css-tricks.ir/tutorial/%d8%a7%d9%86%db%8c%d9%85%db%8c%d8%b4%d9%86-%d8%af%d8%b1-canvas/#respond</comments>
		
		<dc:creator><![CDATA[حسین رفیعی]]></dc:creator>
		<pubDate>Sun, 31 Oct 2021 04:40:17 +0000</pubDate>
				<category><![CDATA[آموزش]]></category>
		<category><![CDATA[animation]]></category>
		<category><![CDATA[canvas]]></category>
		<category><![CDATA[انیمیشن]]></category>
		<guid isPermaLink="false">https://css-tricks.ir/?p=9440</guid>

					<description><![CDATA[<p>در این آموزش به زیر و بم ساخت انیمیشن‌ها، ترفند‌های اجرای بهتر، و راه‌حل برخی از مشکلات آن در canvas می‌پردازیم.</p>
<p>نوشته <a rel="nofollow" href="https://css-tricks.ir/tutorial/%d8%a7%d9%86%db%8c%d9%85%db%8c%d8%b4%d9%86-%d8%af%d8%b1-canvas/">انیمیشن در canvas</a> اولین بار در <a rel="nofollow" href="https://css-tricks.ir">آموزش طراحی وب</a> پدیدار شد.</p>
]]></description>
										<content:encoded><![CDATA[<p>پس از قدرت گرفتن مرورگر‌ها امکانات مناسبی برای ساخت انیمیشن ایجاد شد که هم قدرت بالایی دارند، و هم روش استفاده از آن‌ها بسیار آسان است. در این آموزش به زیر و بم ساخت انیمیشن، ترفند‌های اجرای بهتر، و راه‌حل برخی از مشکلات آن می‌پردازیم. این آموزش فقط ویژه‌ی canvas نیست و از این توابع می‌توان برای ایجاد انیمیشن روی هر بخشی در سند استفاده کرد.</p>
<p>از آنجایی که در این آموزش هم به موارد ابتدایی و ساده، و هم به موارد پیشرفته اشاره شده، سعی کنید موارد گفته شده را به آرامی بیاموزید تا هم تسلط بهتری به موضوع به دست آورده و هم نکات مهم را از قلم نیندازید. اینکه درک کامل و مناسبی از موضوع به دست بیاورید مهم‌تر از خواندن یک‌باره‌ی این مطلب است!</p>
<h2>مشکلات توابع پیشین</h2>
<p>سال‌ها پیش، توابع <code>setInterval</code> و <code>setTimeout</code> در کنار فلش و دیگر فناوری‌ها برای ساخت انیمیشن استفاده می‌شدند؛ اما این موارد مشکلات زیادی داشتند که از جمله‌ی آن‌ها می‌توان به این موارد اشاره کرد:</p>
<ul>
<li>این توابع مرورگر را مجبور می‌کردند که تابع انیمیشن را در زمان‌های مشخص‌شده (مثلا هر 10 میلی‌ثانیه یک بار) اجرا کند که باعث فشار روی مرورگر می‌شد و سرعت صفحه پایین می‌آمد.</li>
<li>این توابع حتی هنگامی که صفحه در حال نمایش نبود (مثلا هنگام باز بودن چند تب) باز هم اجرا می‌شدند و پردازش بی‌دلیل ایجاد می‌کردند. چند سند با انیمیشن می‌توانستند به سادگی باعث crash شدن مرورگر شوند.</li>
</ul>
<h2>توابع AnimationFrame</h2>
<p>توابع جدیدی که برای ساخت انیمیشن معرفی شده‌اند، نه‌تنها مشکلات توابع قبلی را ندارند، بلکه بسیار هم سریع و بهینه هستند. این توابع <code>requestAnimationFrame</code> و <code>cancelAnimationFrame</code> نام دارند که تابع اول برای اجرای انیمیشن و تابع دوم برای توقف انیمیشن به کار می‌رود.</p>
<p>استفاده از این توابع بسیار ساده است. کافیست تابعی که می‌خواهیم انیمیشن را اجرا کند، به عنوان ورودی تابع <code>requestAnimationFrame</code> (یا به اختصار <code>rAF</code>) بدهیم. نمونه کد زیر این موضوع را به خوبی نشان می‌دهد:</p>
<pre><code class="language-javascript">
function animated_function () { /* ... */ }
requestAninmationFrame(animated_function);
</code></pre>
<p>بهتر است با یک نمونه‌ی مناسب این تابع را بررسی کنیم. کد زیر را درنظر بگیرید. در تابع <code>draw_rect</code> یک مربع با طول ضلع و رنگ تصادفی ایجاد شده و در یک مختصات تصادفی از صفحه رسم می‌شود. در این کد این تابع را به کمک تابع <code>rAF</code> به صورت انیمیشن اجرا می‌کنیم:</p>
<pre><code class="language-javascript">
function get_random (min, max) {
    return Math.random() * (max - min + 1) + min | 0;
}

function draw_rect () {
    let color = "hsl(" + get_random(0, 360) + ", 100%, 50%)",
        size = get_random(50, 200),
        
        x = get_random(0, cvs.width),
        y = get_random(0, cvs.height);
    
    ctx.fillStyle = color;
    ctx.fillRect(x - size, y - size, size * 2, size * 2);
}

requestAnimationFrame(draw_rect);
</code></pre>
<p>پرسش اصلی اینجاست که چرا فقط یک مربع رسم می‌شود و انیمیشن اجرا نمی‌شود؟ پاسخ در رفتار این تابع نهفته است. همانطور که از نام این تابع پیداست، این تابع فقط یک فریم از انیمیشن را اجرا می‌کند، نه یک انیمیشن کامل! خب پرسش جدید این است که چطور یک انیمیشن کامل با این تابع ایجاد کنیم؟ پاسخ این است که باید تابع <code>rAF</code> درون تابع انیمیشن باشد و با اجرای هر فریم انیمیشن، فریم بعدی را اجرا کند! یعنی کد بالا باید به این شکل اصلاح شود:</p>
<pre><code class="language-javascript">
function get_random (min, max) {
    return Math.random() * (max - min + 1) + min | 0;
}

function draw_rect () {
    let color = "hsl(" + get_random(0, 360) + ", 100%, 50%)",
        size = get_random(50, 200),
        
        x = get_random(0, cvs.width),
        y = get_random(0, cvs.height);
    
    ctx.fillStyle = color;
    ctx.fillRect(x - size, y - size, size * 2, size * 2);
    
    requestAnimationFrame(draw_rect);
}

requestAnimationFrame(draw_rect);
</code></pre>
<p>کد بالا را اجرا کرده و نتیجه را ببینید. شما اولین انیمیشن خود را ایجاد کردید! (البته اگر از انیمیشن‌های آموزش‌های پیشین چشم‌پوشی کنیم!) خب حال باید به ویژگی‌های این تابع و انیمیشن‌ها بپردازیم. این تابع می‌تواند در هر جایی از تابع انیمیشن استفاده شود، تنها نکته‌ی مهم این است که این تابع فقط یک بار به ازای هر فریم انیمیشن اجرا شود. برای نمونه تابع <code>rAF</code> می‌تواند همان ابتدای تابع فراخوانی شود، اما اگر یک حلقه درون تابع انیمیشن وجود داشته باشد، تابع <code>rAF</code> هرگز نباید درون حلقه قرار بگیرد، زیرا باعث می‌شود که در یک فریم، چند درخواست برای فریم بعدی داده شود و پس از تنها چند فریم مرورگر متوقف می‌شود! به طور کلی تا هنگامی که در هر فریم فقط یک درخواست برای فریم بعدی داده شود، مشکلی وجود ندارد.</p>
<p>نکته‌ی دیگر اینکه هر تابعی که درون تابع انیمیشن اجرا شود نیز به صورت انیمیشن اجرا می‌شود. این یعنی کد زیر درست مانند کد بالا رفتار می‌کند. در کد زیر یک تابع به نام <code>loop</code> ایجاد شده که درون آن تابع <code>draw_rect</code> اجرا می‌شود. همانطور که می‌بینید، فریم اول انیمیشن می‌تواند بدون تابع <code>rAF</code> اجرا شود، اما فریم‌های بعدی حتما باید با این تابع اجرا شوند:</p>
<pre><code class="language-javascript">
(function loop () {
    draw_rect();
    requestAnimationFrame(loop);
}) ();
</code></pre>
<h2>متوقف کردن انیمیشن</h2>
<p>حال که شما روش اجرای انیمیشن را آموخته‌اید، باید یاد بگیرید که چطور آن را متوقف کنید! روش متوقف کردن این توابع نیز شبیه به توابع <code>setInterval</code> و <code>setTimout</code> است. تابع <code>rAF</code> یک شناسه‌ی انیمیشن بازمی‌گرداند که باید از این شناسه به عنوان ورودی تابع <code>cancelAnimationFrame</code> استفاده کنیم. این تابع (به اختصار <code>cAF</code>) برای توقف انیمیشن استفاده می‌شود و ورودی آن، شناسه‌ی انیمیشن موردنظر است. به نمونه کد زیر توجه کنید. در این کد، تابع انیمیشن <code>my_animation</code> ده ثانیه پس از اجرا متوقف می‌شود:</p>
<pre><code class="language-javascript">
let anim_id;

function my_animation () {
    /**/
    
    anim_id = requestAnimationFrame(my_animation);
}

requestAnimationFrame(my_animation);

setTimeout(function () {
    cancelAnimationFrame(anim_id);
}, 10000);
</code></pre>
<p>استفاده از تابع <code>cAF</code> ساده‌ترین راه برای توقف انیمیشن است اما تنها راه نیست. موقعیت‌هایی وجود دارند که در آن‌ها این تابع موفق به متوقف کردن انیمیشن نمی‌شود. یکی از این موقعیت‌ها هنگامی است که تابع <code>rAF</code> بیش از یک بار اجرا شده باشد. در این حالت، fps (یا تعداد اجرای انیمیشن در یک ثانیه) تغییر نمی‌کند، (معمولا 60fps است) بلکه در هر فریم، به جای یک بار اجرا، دو بار (یا بیشتر) اجرا می‌شود. در این حالت اگر تابع <code>cAF</code> اجرا شود، فقط یکی از این اجرا‌ها در هر فریم را متوقف می‌کند ولی دیگر اجرا‌ها همچنان ادامه پیدا می‌کنند.</p>
<p>یک راه دیگر برای توقف انیمیشن، استفاده از یک دستور شرطی برای ادامه‌ی انیمیشن است. این دستور بررسی می‌کند که آیا انیمیشن باید اجرا شود یا خیر، در صورت مثبت بودن پاسخ، تابع <code>rAF</code> اجرا شده و در غیر این صورت اجرا نمی‌شود. به نمونه کد زیر توجه کنید. توابع <code>draw_rect</code> و <code>get_random</code> را به این کد اضافه کرده و آن را اجرا کنید:</p>
<pre><code class="language-javascript">
/* include (draw_rect) &amp; (get_random) */

let animation_status = true;

function loop () {
    draw_rect();
    
    if (animation_status) requestAnimationFrame(loop);
}

loop();
</code></pre>
<p>هنگامی که انیمیشن در حال اجراست، وارد console شده و مقدار <code>animation_status</code> را به <code>false</code> تغییر دهید. بعد از این کار انیمیشن متوقف می‌شود. این روش در مقایسه با روش <code>cAF</code> اطمینان بیشتری دارد اما دارای مشکلی نیز هست. اگر از این روش در انیمیشن‌هایی استفاده شود که به نوعی دارای حساسیت هستند، برای نمونه در یک بازی که مکان دشمن یا دیگر موارد مهم است، این روش ممکن است باعث خراب شدن صحنه شود.</p>
<p>بهترین روش استفاده از هر دو گزینه همراه با یک ساختار مناسب است که مانع ارسال درخواست اضافی شود. در ادامه قدم به قدم یک کلاس مناسب برای این موضوع ایجاد می‌کنیم که این مشکل را نیز، همراه با مشکلات احتمالی دیگر، حل می‌کند و پشتیبانی مناسبی نیز دارد.</p>
<h2>بررسی چند نمونه</h2>
<p>در این بخش به ایجاد چند انیمیشن با مواردی که آموخته‌ایم می‌پردازیم. از آنجایی که این موارد از پیش بررسی شده‌اند، توضیحاتی درباره‌ی کد داده نمی‌شود بلکه فقط کاری که انجام می‌دهد بررسی می‌شود.</p>
<h3>رسم دایره‌های تصادفی</h3>
<p>این کد نیز شبیه به کد مربع‌های تصادفی است، با این تفاوت که پیش از رسم شکل فعلی پاک می‌شود و استفاده از دستور <code>stroke</code> لازم است. سعی کنید برای ایجاد تنوع و یادگیری بهتر، کد زیر را تغییر دهید:</p>
<pre><code class="language-javascript">
function get_random (min, max) {
    return Math.random() * (max - min + 1) + min | 0;
}

function draw_arc () {
    let radius = get_random(25, 100),
        color = "hsl(" + get_random(0, 360) + ", 100%, 50%)",
        
        x = get_random(0, cvs.width),
        y = get_random(0, cvs.height);
    
    ctx.beginPath();
    ctx.arc(x, y, radius, 0, Math.PI * 2);
    
    ctx.strokeStyle = color;
    ctx.lineWidth = radius / 20 | 0;
    
    ctx.stroke();
    requestAnimationFrame(draw_arc);
}

draw_arc();
</code></pre>
<h3>رسم خطوط تصادفی با حاشیه</h3>
<p>این کد نسبتا ساده است و فقط یک زنجیره از خطوط ایجاد می‌کند. که در هر فریم یک خط به آن‌ها اضافه می‌شود. در این کد از شفافیت زیادی برای ویژگی <code>fillStyle</code> استفاده شده (95% شفافیت) و در هر فریم یک مربع روی لایه‌ی ترسیمات رسم می‌شود که باعث ایجاد یک جلوه در ناپدید شدن خطوط شده است. با استفاده از مقدار <code>destination-out</code> نیز می‌توان به نتیجه‌ی مشابهی رسید اما این روش کمی بهینه‌تر است:</p>
<pre><code class="language-javascript">
function get_random (min, max) {
    return Math.random() * (max - min + 1) + min | 0;
}

let line_chain = [],
    max_chain_size = 5;

ctx.lineWidth = 5;
ctx.lineJoin = ctx.lineCap = "round";

ctx.strokeStyle = "#FFF";
ctx.fillStyle = "rgba(0, 0, 0, 0.05)";

function draw_lines () {
    let x = get_random(0, cvs.width),
        y = get_random(0, cvs.height);
    
    line_chain.push([x, y]);
    if (line_chain.length &gt; max_chain_size) line_chain.shift();
    
    ctx.beginPath();
    
    for (let i = 0, l = line_chain.length; i &lt; l; i++) {
        ctx.lineTo(...line_chain[i]);
    }
    
    ctx.fillRect(0, 0, cvs.width, cvs.height);
    
    ctx.stroke();
    requestAnimationFrame(draw_lines);
}

draw_lines();
</code></pre>
<h3>رسم خطوط با خط‌چین توسط نشانگر</h3>
<p>این کد ابتدا یک خط در نقطه‌ای تصادفی انتخاب می‌کند و قلم را به آنجا می‌برد، حال با هر کلیک روی صفحه، یک خط به مختصات نشانگر موس رسم می‌شود. انیمیشن این برنامه نیز روی حاشیه‌ی این شکل متمرکز است و به آن انیمیشن می‌دهد. شیوه‌ی ایجاد انیمیشن روی خط‌چین پیش از این بررسی شده و نیاز به توضیح ندارد:</p>
<pre><code class="language-javascript">
function get_random (min, max) {
    return Math.random() * (max - min + 1) + min | 0;
}

function get_cursor (e) {
    let box = cvs.getBoundingClientRect(),
        
        x = e.clientX - box.left,
        y = e.clientY - box.top;
    
    ctx.lineTo(x, y);
}

function draw_dash_lines () {
    ctx.clearRect(0, 0, cvs.width, cvs.height);
    
    ctx.lineDashOffset = -offset;
    ctx.stroke();
    
    offset = ++offset % max_dash;
    requestAnimationFrame(draw_dash_lines);
}

ctx.setLineDash([
    get_random(10, 20),
    get_random(20, 30),
    get_random(15, 25)
]);

let offset = 0,
    max_dash = ctx.getLineDash().reduce((a, b) =&gt; a + b);

ctx.moveTo(get_random(0, cvs.width), get_random(0, cvs.height));
cvs.addEventListener("click", get_cursor);

draw_dash_lines();
</code></pre>
<h3>دایره‌های سرگردان</h3>
<p>این نمونه کمی پیچیده‌تر است اما یادگیری آن بسیار ضروری است زیرا بیشتر ترسیمات پویا در canvas با این روش‌ها ایجاد می‌شوند. در این کد با هر حرکت موس در صفحه، چند دایره با شعاع تصادفی، در جهت‌های تصادفی حرکت کرده و کوچک می‌شوند تا اینکه ناپدید شوند. در ادامه برای بخش‌های مورد نیاز توضیحاتی آورده شده است:</p>
<pre><code class="language-javascript">
cvs.width = cvs.height = 700;

/* (1) */
function get_random (min, max) {
    return ((Math.random() * (max - min + 1) + min) * 100 | 0) / 100;
}

class Circle {
    /* (2) */
    constructor (x, y) {
        this.x = x;
        this.y = y;
        
        this.r = get_random(5, 15);
        this.rs = get_random(0.2, 2.2);
        
        this.v = [get_random(-5, 5), get_random(-5, 5)];
        this.status = true;
    }
    
    /* (3) */
    update () {
        if (this.r === 0) {
            this.status = false;
            return;
        }
        
        this.x += this.v[0];
        this.y += this.v[1];
        
        this.r = Math.max(0, this.r - this.rs);
    }
    
    /* (4) */
    draw () {
        ctx.moveTo(this.x + this.r, this.y);
        ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
    }
}

function draw_arcs () {
    /* (5) */
    ctx.fillStyle = "#111";
    ctx.fillRect(0, 0, cvs.width, cvs.height);
    
    ctx.beginPath();
    ctx.fillStyle = "#FFF";
    
    /* (6) */
    for (let i = arcs.length - 1; i &gt; -1; i--) {
        if (!arcs[i].status) {
            arcs.splice(i, 1);
            continue;
        }
        
        arcs[i].update();
        arcs[i].draw();
    }
    
    /* (7) */
    ctx.fill();
    requestAnimationFrame(draw_arcs);
}

/* (8) */
function add_arcs (e) {
    let box = cvs.getBoundingClientRect(),
        
        x = e.clientX - box.left,
        y = e.clientY - box.top;
    
    /* (9) */
    for (let i = 0; i &lt; 100; i++) {
        arcs.push(new Circle(x, y));
    }
}

/* (10) */
let arcs = [];

cvs.addEventListener("mousemove", add_arcs);
requestAnimationFrame(draw_arcs);
</code></pre>
<h4>بخش 1 تابع جدید اعداد تصادفی</h4>
<p>این تابع تفاوت کوچکی با تابع پیشین دارد. در این تابع اعداد تولید‌شده دارای دو رقم اعشار هستند. در برنامه‌های قبل می‌شد از این مورد چشم‌پوشی کرد اما در این برنامه وجود قسمت اعشاری در اعداد بهتر است.</p>
<h4>بخش 2 کلاس Circle</h4>
<p>در این بخش یک کلاس به نام <code>Circle</code> ایجاد می‌کنیم. این کلاس یک مختصات به عنوان ورودی دریافت کرده و یک شئ برمی‌گرداند که دارای ویژگی‌های <code>(x,y)</code>، <code>r</code>، <code>rs</code>، <code>v</code>، و <code>status</code> است. به جز مختصات مرکز و <code>status</code>، دیگر ویژگی‌های این شئ تصادفی هستند. ویژگی <code>r</code> شعاع دایره، ویژگی <code>v</code> سرعت و جهت حرکت دایره، ویژگی <code>rs</code> سرعت کوچک شدن شعاع، و ویژگی <code>status</code> وضعیت دایره را نشان می‌دهد.</p>
<h4>بخش 3 متد update</h4>
<p>این متد برای به‌روز‌رسانی مختصات و وضعیت دایره است. این متد ابتدا مثبت بودن شعاع دایره را بررسی می‌کند. اگر شعاع صفر بود، وضعیت یا <code>status</code> دایره را به <code>false</code> تغییر داده و اجرای متد را متوقف می‌کند. در غیر این صورت، مختصات دایره را توسط سرعت تغییر داده، و شعاع آن را به اندازه‌ی <code>rs</code> کوچک می‌کند؛ البته به کمک متد <code>Math.max</code> اطمینان حاصل می‌کند که شعاع منفی نشود.</p>
<h4>بخش 4 متد draw</h4>
<p>وظیفه‌ی این متد، قرار دادن یک دایره با مشخصات موردنظر در شکل فعلی است. این متد ابتدا با کمک متد <code>moveTo</code>، قلم را به زاویه‌ی آغاز دایره برده و سپس یک دایره رسم می‌کند تا از رسم خطوط اضافی جلوگیری کند. از آنجایی که شکل فعلی در تابع انیمیشن پاک می‌شود، نیازی به پاک کردن آن درون این متد نیست.</p>
<h4>بخش 5 تابع رسم دایره‌ها</h4>
<p>در این بخش تابع <code>draw_arcs</code> را تعریف می‌کنیم. وظیفه‌ی این تابع، به‌روز‌رسانی دایره‌ها و رسم آن‌ها، و در صورت نیاز حذف دایره‌هایی است که شعاع آن‌ها به صفر رسیده. این تابع ابتدا ویژگی <code>fillStyle</code> را به سیاه تغییر داده و یک مربع روی کل لایه‌ی ترسیمات رسم می‌کند، سپس آن را به سفید تغییر می‌دهد و شکل فعلی را برای رسم دایره‌های جدید پاک می‌کند.</p>
<h4>بخش 6 بررسی دایره‌ها</h4>
<p>در این بخش یک حلقه از پایان حلقه تا آغاز آن اجرا می‌شود و هرکدام از دایره‌هایی که در آرایه‌ی <code>arcs</code> هستند را بررسی می‌کند. اگر ویژگی <code>status</code> هر دایره برابر <code>false</code> باشد، آن را از آرایه حذف می‌کند و به سراغ دایره‌ی بعدی می‌رود. سپس متد <code>draw</code> و <code>update</code> را روی دایره‌های مناسب اجرا می‌کند.</p>
<h4>بخش 7 رسم دایره‌ها</h4>
<p>در این بخش با استفاده از متد <code>fill</code> تمام دایره‌هایی که در شکل فعلی قرار گرفته‌اند رسم می‌شوند. سپس به کمک تابع <code>rAF</code> فریم بعدی از انیمیشن (از همین تابع) درخواست می‌شود.</p>
<h4>بخش 8 اضافه کردن دایره‌ها</h4>
<p>در این بخش تابع <code>add_arcs</code> ایجاد می‌شود. این تابع هنگامی اجرا می‌شود که موس روی عنصر <code>cvs</code> حرکت کند. این تابع ابتدا به کمک <code>getBoundingClientRect</code> و ویژگی‌های ورودی <code>e</code> (از مدیریت رویداد) مختصات موس روی عنصر را به دست می‌آورد و در متغیر‌های <code>x</code> و <code>y</code> ذخیره می‌کند.</p>
<h4>بخش 9 دایره به تعداد دلخواه</h4>
<p>در این بخش یک حلقه به طول دلخواه ایجاد می‌شود. درون این حلقه، به کمک کلاس <code>Circle</code> دایره در مختصات <code>(x,y)</code> ایجاد شده و به آرایه‌ی <code>arcs</code> اضافه می‌شود. می‌توانید طول حلقه را هرچقدر خواستید تغییر دهید اما مراقب باشید چون تعداد خیلی زیاد ممکن است باعث توقف مرورگر شود!</p>
<h4>بخش 10 آغاز برنامه</h4>
<p>در این بخش آرایه‌ی <code>arcs</code> ایجاد می‌شود، سپس مدیریت رویداد <code>mouse-move</code> روی عنصر <code>cvs</code> ایجاد شده و انیمیشن تابع <code>draw_arcs</code> آغاز می‌شود. حال برنامه در حال اجراست و با هر حرکت موس روی عنصر، تعداد 100 دایره با سرعت و شعاع تصادفی در آن نقطه ایجاد شده و شروع به حرکت می‌کنند.</p>
<p>همانطور که می‌بینید، با چند دستور ساده می‌توان چنین جلوه‌های زیبایی ساخت. در آموزش‌های آینده نمونه‌های بیشتری بررسی خواهد شد، اما لازم است ابتدا با نمونه‌های ساده‌تر، مانند این نمونه، آشنا باشید. در زیر یک نمونه‌ی پیشرفته‌تر که با همین روش ایجاد شده می‌بینید:</p>
<p class="codepen" data-height="467" data-default-tab="result" data-slug-hash="xxroNav" data-preview="true" data-user="Hossein_Rafie" style="height: 467px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/Hossein_Rafie/pen/xxroNav"><br />
  Canvas Shape Sparkle</a> by Hossein Rafie (<a href="https://codepen.io/Hossein_Rafie">@Hossein_Rafie</a>)<br />
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<p><script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script></p>
<h2>زمان‌بندی در انیمیشن</h2>
<p>انیمیشن‌هایی که با توابع AnimationFrame ساخته می‌شوند، نسبت به پردازش موردنیاز انیمیشن اجرا می‌شوند. این انیمیشن‌ها معمولا 60fps هستند اما این مقدار ممکن است نسبت به کیفیت نمایشگر کاربر بیشتر باشد. همچنین اگر انیمیشن سنگین باشد، این مقدار پایین می‌آید. خب یک پرسش مهم به ذهن می‌آید: «چطور می‌توانیم مستقل از fps انیمیشن، آن را با سرعت ثابت اجرا کنیم؟»</p>
<p>خب چرا باید بخواهیم انیمیشن با سرعت ثابت اجرا شود؟ یک بازی تحت وب را در نظر بگیرید. حال فرض کنید سه نفر در حال اجرای این بازی هستند. نفر اول یک سیستم عادی در اختیار دارد که انیمیشن را با سرعت 60fps اجرا می‌کند. نفر دوم چند پردازش سنگین در پس‌زمینه‌ی خود دارد و سرعت انیمیشن برای او به 30fps رسیده است. نفر سوم یک سیستم به‌روز و قدرتمند دارد که در حالت عادی انیمیشن را با سرعت 90fps اجرا می‌کند. اگر سرعت بازی به fps انیمیشن بستگی داشته باشد، یعنی سرعت بازی برای این سه نفر متفاوت است. این موضوع می‌تواند باعث بازخورد منفی فراوان شود و باید به دنبال یک راه برای اجرای انیمیشن با سرعت ثابت باشیم.</p>
<p>برای اینکه سرعت انیمیشن ثابت شود، باید یک متغیر از نوع زمان وارد آن کنیم. خوشبختانه تابع <code>rAF</code> این کار را انجام می‌دهد و یک ورودی عددی از نوع <code>HighResTimeStamp</code> به عنوان ورودی تابع انیمیشن می‌دهد که زمان گذشته از آغاز انیمیشن را به ما می‌دهد. البته می‌توان از روش‌های دیگر مانند <code>Date.now</code> نیز استفاده کرد اما استفاده از ورودی دقیق‌تر است. به نمونه کد زیر دقت کنید. در کد زیر شعاع دایره در آغاز 50 است و پس از 5 ثانیه به 250 می‌رسد:</p>
<pre><code class="language-javascript">
cvs.width = cvs.height = 500;

let radius = 50,
    duration = 5,
    
    min_radius = 50,
    max_radius = 250,
    
    last_time = 0,
    delta;

function grow_arc (t) {
    ctx.clearRect(0, 0, 500, 500);
    
    ctx.beginPath();
    ctx.arc(250, 250, radius, 0, Math.PI * 2);
    ctx.fill();
    
    delta = Math.abs(t - last_time) / (1000 * duration);
    
    if (radius &lt; max_radius) {
        last_time = t;
        requestAnimationFrame(grow_arc);
    }
    
    radius += (max_radius - min_radius) * delta;
}

requestAnimationFrame(grow_arc);
</code></pre>
<p>در کد بالا دو متغیر <code>last_time</code> و <code>delta</code> مسئول زمان‌بندی انیمیشن هستند. متغیر <code>last_time</code> آخرین مقدار زمانی‌ای که در ورودی <code>t</code> بوده را دارد. این مقدار در آغاز انیمیشن صفر است بنابراین مقدار اولیه‌ی این متفیر نیز صفر است. متغیر <code>delta</code> نیز اختلاف ورودی <code>t</code> و <code>last_time</code> است. این متغیر زمانی که از اجرای آخرین فریم گذشته را در یکای میلی‌ثانیه نشان می‌دهد؛ به همین دلیل تقسیم بر 1000 می‌شود تا یکای آن ثانیه شود.</p>
<p>همانطور که می‌دانید، شعاع دایره باید در مدت پنج ثانیه به اندازه‌ی 200 افزایش یابد. برای این کار عبارت <code>max_radius – min_radius</code> یا همان 200 را تقسیم بر <code>delta * duration</code> می‌کنیم و نتیجه را به شعاع دایره اضافه می‌کنیم. حال ممکن است بپرسید نقش <code>delta * duration</code> چیست؟ برای پاسخ به آن فرض کنید طول انیمیشن یک ثانیه، یعنی <code>duration</code> برابر 1 باشد.</p>
<p>با این فرض، و فرض اینکه انیمیشن با سرعت 60fps اجرا می‌شود، می‌بینیم که در هر فریم 200/60 مقدار به شعاع اضافه می‌شود. وقتی سرعت انیمیشن 60fps باشد، یعنی مقدار 200/60 در یک ثانیه 60 بار به شعاع اضافه می‌شود. این یعنی مقداری که در یک ثانیه به شعاع اضافه شده برابر 200 * 60 / 60 یا همان 200 است! این نتیجه‌گیری روی هر سرعتی در انیمیشن جواب می‌دهد، حتی در انیمیشن‌های با سرعت متغیر!</p>
<p>خب نقش <code>duration</code> چیست؟ این متغیر مدت زمان انیمیشن (یا یک حرکت در انیمیشن) را به ثانیه مشخص می‌کند. البته در حالت کلی از این متغیر استفاده نمی‌شود، بلکه سعی می‌شود مقدار تغییرات با این متغیر ترکیب شود. یعنی به جای اینکه بگوییم شعاع در مدت پنج ثانیه 200 واحد تغییر کند، می‌گوییم در مدت یک ثانیه 40 واحد تغییر کند. با انجام این کار نیازی به متغیر <code>duration</code> نیست و همان نتیجه به دست می‌آید.</p>
<p>نکته‌ی مهمی که باید به آن توجه کنید، این است که به طور کلی زمان و کار با آن نتایج بسیار دقیقی به دست نمی‌دهند. این مورد نه فقط در توابع AnimationFrame بلکه در همه‌جا وجود دارد فقط مقدار خطا متفاوت است. در این دست انیمیشن‌ها نیز مقدار خطا، بسته به نوع کدی که نوشته شده، می‌تواند تا 30 میلی‌ثانیه باشد که البته بسیار کوچک است.</p>
<p>از آنجایی که خطا در محاسبات (هرچند کوچک) می‌تواند باعث بروز خطا در برخی برنامه‌ها شود، توصیه می‌شود کران‌های مقداری را بهتر وارد برنامه کنید. برای نمونه به کد زیر دقت کنید. در این کد یک دایره به شعاع 40 و رنگ زرد یک حرکت تصادفی در صفحه آغاز می‌کند و پس از برخورد به دیواره‌های صفحه، حرکت آن بازتابی می‌شود. با کلیک روی عنصر می‌توانید سرعت آن را تغییر دهید:</p>
<pre><code class="language-javascript">
function get_random (min, max) {
    return Math.random() * (max - min + 1) + min | 0;
}

const X = 0, Y = 1;

let circle = {
        x: cvs.width / 2,
        y: cvs.height / 2,
        
        r: 40,
        c: "#CC0",
        
        init: function () {
            this.min = [this.r, this.r];
            this.max = [cvs.width - this.r, cvs.height - this.r];
            
            this.v = [get_random(-300, 300), get_random(-300, 300)];
        },
        
        draw: function () {
            ctx.clearRect(0, 0, cvs.width, cvs.height);
            
            ctx.beginPath();
            ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
            
            ctx.fillStyle = this.c;
            ctx.fill();
        },
        
        update: function (d) {
            this.x = Math.max(this.min[X], Math.min(this.x + this.v[X] * d, this.max[X]));
            this.y = Math.max(this.min[Y], Math.min(this.y + this.v[Y] * d, this.max[Y]));
            
            if (this.x === this.min[X] || this.x === this.max[X]) this.v[X] *= -1;
            if (this.y === this.min[Y] || this.y === this.max[Y]) this.v[Y] *= -1;
        }
    },
    
    last_time = 0,
    delta;

function show_circle (t) {
    delta = (t - last_time) / 1000;
    
    circle.draw();
    circle.update(delta);
    
    last_time = t;
    requestAnimationFrame(show_circle);
}

circle.init.call(circle);
requestAnimationFrame(show_circle);

cvs.addEventListener("click", circle.init.bind(circle));
</code></pre>
<p>همانطور که می‌بینید سرعت این انیمیشن ثابت است و این یعنی در آن خطای محاسباتی وجود دارد. اگر کران‌های مقداری دایره به خوبی استفاده نشوند، در fps‌های پایین ممکن است برنامه خراب شود. مثلا توپ درون یکی از دیواره‌ها گیر کند. با بررسی مناسب کد متوجه علت آن خواهید شد! برای حل این مشکل از متد‌های <code>Math.max</code> و <code>Math.min</code> استفاده می‌کنیم تا مختصات دایره هیچگاه خارج از محدوده‌ی کران‌هایش قرار نگیرد.</p>
<p>در تابع انیمیشن <code>show_circle</code>، متغیر <code>delta</code> حساب شده و به عنوان ورودی متد <code>update</code> شئ <code>circle</code> استفاده می‌شود. با این کار دیگر لازم نیست درون متد <code>update</code> درگیر محاسبه‌ی <code>delta</code> شویم و می‌توانیم آن را مستقیما به عنوان ورودی متد داشته باشیم! در ادامه بیشتر به این موضوع می‌پردازیم.</p>
<p>در کد بالا ویژگی‌های <code>x</code> و <code>y</code> مختصات مرکز دایره، ویژگی <code>c</code> رنگ دایره و ویژگی <code>r</code> شعاع دایره هستند. برخی تعریف‌ها لازم است درون یک متد انجام شوند، بنابراین متد <code>init</code> را تعریف می‌کنیم. این متد بعدها در هر رویداد کلیک نیز اجرا خواهد شد. درون این متد سرعت با ویژگی <code>v</code> و کران یا محدوده‌ی شکل نیز درون ویژگی‌های <code>min</code> و <code>max</code> تعریف می‌شود.</p>
<p>متد <code>draw</code> وظیفه‌ی رسم دایره را بر عهده دارد و متد <code>update</code> نیز مختصات مرکز دایره را بر اساس سرعت و کران‌های آن به‌روز‌رسانی می‌کند. به جز این موارد، ممکن است کارکرد متد‌های <code>bind</code> و <code>call</code> برای شما مشخص نباشند. این متد‌ها در کنار متد <code>apply</code> سه متد قدرتمند و مهم در شئ‌گرایی جاوااسکریپت هستند و برای درک روش کار آن‌ها تسلط به شئ‌گرایی لازم است و نمی‌توان توضیحات بیشتری داد. برای خوانندگان علاقمند و مسلط خواندن <a href="http://javascriptissexy.com/javascript-apply-call-and-bind-methods-are-essential-for-javascript-professionals/">این مطلب</a> را پیشنهاد می‌کنیم.</p>
<h2>حالت focus صفحه</h2>
<p>همانطور که گفتیم، توابع AnimationFrame برای ایجاد انیمیشن بهینه هستند. یکی از این موارد بهینه‌سازی شده در این توابع، این است که این توابع وقتی صفحه از حالت <code>focus</code> خارج می‌شود، بسیار کند می‌شوند؛ یعنی متوقف می‌شوند، اما بسته به مرورگر، در هر دقیقه حدود سه فریم از آن‌ها اجرا می‌شود. البته برخی مرورگر‌ها آن‌ها را سریع‌تر اجرا می‌کنند. حال اگر در این انیمیشن‌ها از <code>delta</code> استفاده کرده باشید باید منتظر فاجعه باشید! زیرا <code>delta</code> همان فاصله‌ی زمانی است و وقتی یک فریم از انیمیشن بیست ثانیه بعد اجرا شود، بیست برابر سرعت (یا تغییرات) به اشیاء درون انیمیشن اعمال شده و ممکن است کل انیمیشن خراب شود.</p>
<p>راه‌حل این موضوع می‌تواند استفاده از ویژگی <code>visibilityState</code> باشد. به‌گونه‌ای که اگر صفحه در حال نمایش نبود، انیمیشن‌ها دیگر اجرا نشوند. می‌توان از رویکرد مشابه برای توقف انیمیشن‌ها با دستور شرطی استفاده کرد. یا اینکه می‌توان از رویداد‌های <code>focus</code> و <code>blur</code> به شکل مشابه استفاده کرد. مشکل اصلی اینجاست که راه‌حل نسبت به انیمیشنی که تعریف کرده‌ایم تغییر می‌کند. در برخی انیمیشن‌ها حتی نیاز به استفاده از متغیر <code>delta</code> نیست ولی در دیگر انیمیشن‌ها وجود آن حیاتی است. توقف برخی انیمیشن‌ها مهم نیست، ولی برخی دیگر نمی‌توانند و نباید متوقف شوند؛ مانند یک بازی آنلاین.</p>
<p>در کنار این موارد، گاهی لازم است ساختار انیمیشن، بسته به نوع آن، تغییر کند. برای نمونه گاهی می‌توان پردازشات مربوط به انیمیشن را از بخش ترسیمات آن جدا کرد. مثلا می‌توان از <code>setInterval</code> برای پردازشات، و از <code>rAF</code> برای رسم آن استفاده کرد. البته در کار با این موارد باید مراقب باشید زیرا ممکن است بازده برنامه پایین بیاید.</p>
<h2>کلاس Animation</h2>
<p>خب، نگاهی به متد <code>update</code> و ورودی آن در کد بالا بیندازید. چرا باید برای هر انیمیشن دو متغیر <code>last_time</code> و <code>delta</code> تعریف کنیم و در هر فریم آن‌ها را حساب کنیم؟ بهتر نیست این جریان را خودکار کنیم و مانند متد <code>update</code> آن را به صورت ورودی داشته باشیم؟ می‌توانیم برای حل این مورد و همچنین حل دیگر مشکلات انیمیشن (مانند توقف انیمیشن) یک کلاس ایجاد کرده و از آن برای ایجاد انیمیشن استفاده کنیم.</p>
<p>این کلاس یک شئ ایجاد می‌کند که دو ویژگی و دو متد دارد. ویژگی <code>callback</code> که همان تابع انیمیشن است و ویژگی <code>status</code> نیز وضعیت اجرای انیمیشن را نشان می‌دهد. متد <code>start</code> برای آغاز انیمیشن و متد <code>stop</code> برای توقف آن است. کاری که این کلاس انجام می‌دهد، این است که یک لایه‌ی کنترلی به تابع انیمیشن اضافه می‌کند و اجازه نمی‌دهد تابع <code>rAF</code> برای آن تابع بیش از یک بار اجرا شود. همچنین هنگام متوقف کردن آن، هم از دستور شرطی و هم از تابع <code>cAF</code> استفاده می‌کند تا مشکلی در توقف انیمیشن ایجاد نشود.</p>
<p>در کنار این کار‌ها، این کلاس مقدار <code>delta</code> را حساب می‌کند و به عنوان ورودی تابع انیمیشن قرار می‌دهد. این یعنی می‌توانیم از این مقدار مستقیما استفاده کنیم و نیازی به محاسبات اضافی نیست. درضمن نباید درون تابع انیمیشن از تابع <code>rAF</code> استفاده کنیم زیرا این کلاس این کار را انجام می‌دهد:</p>
<pre><code class="language-javascript">
/*
    -- Animation v0.1
    -- by Hossein_Rafie (https://codepen.io/Hossein_Rafie)
*/

class Animation
{
    constructor (animation_function, this_value = null) {
        this.#callback = animation_function.bind(this_value);
        this.#status = false;
    }

    #last = 0;
    #delta;

    #status;
    #callback;

    #anim_id;
    #start_id;

    #run = (t) =&gt; {
        this.#delta = ~~((t - this.#last) * 100) / 100000;
        this.#last = t;
        
        if (!this.#status) return;

        this.#callback.call(null, this.#delta);
        this.#anim_id = requestAnimationFrame(this.#run);
    }

    get status () { return this.#status; }
    get callback () { return this.#callback; }

    start () {
        if (this.#status) return;
        
        if (this.#start_id) this.#last = Date.now() - this.#start_id;
        else this.#start_id = Date.now();

        this.#status = true;
        this.#anim_id = requestAnimationFrame(this.#run);
    }

    stop () {
        if (!this.#status) return;

        this.#status = false;
        cancelAnimationFrame(this.#anim_id);
    }
}
</code></pre>
<p>توضیح خاصی درباره‌ی کد نوشته‌شده نمی‌دهیم زیرا مسائل مربوط به آن خارج از مبحث این آموزش هستند. در کد زیر از این کلاس برای ایجاد انیمیشن روی دایره استفاده می‌کنیم، البته باید تابع <code>show_circle</code> را نیز تغییر دهیم:</p>
<pre><code class="language-javascript">
function show_circle (d) {
    circle.draw();
    circle.update(d);
}

let circle_animate = new Animation(show_circle);
circle_animate.start();
</code></pre>
<p>نکته‌ی آخر اینکه استفاده از این کلاس خوب است، اما بهتر است ابتدا از توابع <code>rAF</code> و <code>cAF</code> استفاده کنید تا به خوبی کارکرد آن‌ها را بیاموزید، سپس برای راحتی کار از این کلاس استفاده کنید. درضمن این کلاس بهترین بازده را ندارد، پس برای بهینه‌سازی بهتر تغییرات موردنظر خود را روی آن اعمال کرده و با ما به اشتراک بکذارید!</p>
<p>به علاوه، استفاده از این کلاس در همه‌جا مناسب نیست، به ویژه در مواردی که تعداد انیمیشن‌ها بسیار زیاد است، زیرا تعریف تعداد زیادی انیمیشن با این کلاس ممکن است حافظه‌ی نسبتا زیادی اشغال کند. احتمالا در آینده به یکی از این موارد برخورد کنیم. در چنین مواردی بهتر است از تابع rAF استفاده کنید، یا یک تابع کلی برای ایجاد انیمیشن ایجاد کرده و از این کلاس روی آن استفاده کنید.</p>
<h2>نتیجه‌گیری</h2>
<p>در این بخش سعی بر آن بود که زیر و بم انیمیشن همراه با چندین نمونه بررسی شود. لطفا این بخش را به خوبی مطالعه کرده و سعی کنید درک مناسبی از ویژگی‌های انیمیشن به دست آورید زیرا کاربرد این موارد بیشتر از آن است که فکر می‌کنید! خوشبختانه روند آموزش رو به پایان است و می‌توانید به این آموزش به چشم دیو سفید از هفت خان این دوره نگاه کنید! در آموزش بعدی به کلاس <code>ImageData</code> و برخی کاربرد‌های آن می‌پردازیم.</p>
<p>نوشته <a rel="nofollow" href="https://css-tricks.ir/tutorial/%d8%a7%d9%86%db%8c%d9%85%db%8c%d8%b4%d9%86-%d8%af%d8%b1-canvas/">انیمیشن در canvas</a> اولین بار در <a rel="nofollow" href="https://css-tricks.ir">آموزش طراحی وب</a> پدیدار شد.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.ir/tutorial/%d8%a7%d9%86%db%8c%d9%85%db%8c%d8%b4%d9%86-%d8%af%d8%b1-canvas/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>متد‌های save و restore در canvas</title>
		<link>https://css-tricks.ir/tutorial/%d9%85%d8%aa%d8%af%e2%80%8c%d9%87%d8%a7%db%8c-save-%d9%88-restore-%d8%af%d8%b1-canvas/</link>
					<comments>https://css-tricks.ir/tutorial/%d9%85%d8%aa%d8%af%e2%80%8c%d9%87%d8%a7%db%8c-save-%d9%88-restore-%d8%af%d8%b1-canvas/#respond</comments>
		
		<dc:creator><![CDATA[حسین رفیعی]]></dc:creator>
		<pubDate>Sat, 23 Oct 2021 03:46:04 +0000</pubDate>
				<category><![CDATA[آموزش]]></category>
		<category><![CDATA[canvas]]></category>
		<category><![CDATA[restore]]></category>
		<category><![CDATA[save]]></category>
		<category><![CDATA[قطبی]]></category>
		<guid isPermaLink="false">https://css-tricks.ir/?p=9425</guid>

					<description><![CDATA[<p>در این بخش به دو متد قدرتمند در canvas به نام‌های save و restore و کاربرد‌های آن‌ها در کنار مفاهیم جدید می‌پردازیم.</p>
<p>نوشته <a rel="nofollow" href="https://css-tricks.ir/tutorial/%d9%85%d8%aa%d8%af%e2%80%8c%d9%87%d8%a7%db%8c-save-%d9%88-restore-%d8%af%d8%b1-canvas/">متد‌های save و restore در canvas</a> اولین بار در <a rel="nofollow" href="https://css-tricks.ir">آموزش طراحی وب</a> پدیدار شد.</p>
]]></description>
										<content:encoded><![CDATA[<p>در این آموزش به یکی از بهترین و پرکاربرد‌ترین امکانات canvas می‌پردازیم. متاسفانه (یا خوشبختانه) گستردگی این دو متد آنقدر بالاست که یادگیری آن‌ها نیازمند دانش نسبتا بالایی از canvas است و لازم است پیش از یادگیری آن‌ها، تقریبا تمام آموزش‌های پیشین را به خوبی آموخته باشید.</p>
<p>موقعیت‌های زیادی پیش می‌آید که بخواهید از تبدیلات ساده استفاده کنید، اما نتوانید ماتریس‌های پیچیده به‌کار ببرید، یا بخواهید از متد <code>clip</code> استفاده کنید، اما نتوانید به حالت قبل بازگردید، یا بخواهید یک کتابخانه‌ی کوچک برای canvas بسازید اما ذخیره‌سازی ویژگی‌های مختلف دردسرساز شود، به طور خلاصه، بخواهید ترسیمات کمی متفاوت ایجاد کرده و به حالت قبل بازگردید!</p>
<h2>متد save</h2>
<p>این متد یک آرایه از اطلاعات فعلی canvas ذخیره می‌کند. این متد هیچ ورودی‌ای نداشته و به تعداد دلخواه می‌تواند فراخوانی شود و فارغ از اینکه آرایه‌ی اطلاعات تکراری است یا خیر، موارد زیر را ذخیره می‌کند:</p>
<ul>
<li>ماتریس تبدیلات فعلی</li>
<li>محدوده‌ی فعلی لایه‌ی ترسیمات (ویژه متد <code>clip</code>)</li>
<li>خط‌چین تعریف شده</li>
<li>مقدار تمام ویژگی‌های <code>strokeStyle</code>، <code>fillStyle</code>، <code>globalAlpha</code>، <code>lineWidth</code>، <code>lineCap</code>، <code>lineJoin</code>، <code>miterLimit</code>، <code>lineDashOffset</code>، <code>shadowOffsetX</code>، <code>shadowOffsetY</code>، <code>shadowBlur</code>، <code>shadowColor</code>، <code>globalCompositeOperation</code>، <code>font</code>، <code>textAlign</code>، <code>textBaseline</code>، <code>direction</code>، <code>imageSmoothingEnabled</code></li>
</ul>
<p>نکته‌ی قابل توجه اینکه ویژگی <code>imageSmoothingEnabled</code> تا حدودی شبیه به ویژگی image-rendering در CSS عمل می‌کند اما پشتیبانی آن نسبتا پایین است. درضمن ویژگی <code>direction</code> نیز شبیه به ویژگی <a href="https://css-tricks.ir/reference/direction/">direction</a> در CSS عمل کرده و جهت متن را مشخص می‌کند، اما هنوز در مرحله‌ی آزمایشی قرار دارد و پشتیبانی از آن ضعیف است. به همین دلیل تاکنون به آن‌ها نپرداخته‌ایم.</p>
<h2>متد restore</h2>
<p>این متد آخرین آرایه‌ی اطلاعات ذخیره‌شده توسط متد <code>save</code> را به canvas اعمال می‌کند. پس از اجرای این متد، آرایه‌ای که اعمال شده از حافظه حذف می‌شود. اگر چیزی ذخیره نشده باشد، این متد کاری انجام نمی‌دهد. به نمونه کد زیر توجه کنید. در کد زیر یک حالت از canvas ذخیره شده، و پس از اینکه ویژگی <code>fillStyle</code> تغییر کرد، دوباره اعمال می‌شود و آن را تغییر می‌دهد:</p>
<pre><code class="language-javascript">
ctx.fillStyle = "#0AF"; /* BLUE */
ctx.save(); /* SAVE CURRENT STATE */

ctx.fillStyle = "#DD0"; /* YELLOW */
ctx.fillRect(25, 25, 200, 200);

ctx.restore(); /* RESTORE PREVIOUS STATE */
ctx.fillRect(275, 25, 200, 200);
/* fillStyle = BLUE */
</code></pre>
<p>در کد بالا ابتدا ویژگی <code>fillStyle</code> به آبی تعیین می‌شود، و سپس متد <code>save</code> این حالت را ذخیره می‌کند. سپس ویژگی <code>fillStyle</code> به زرد تعیین شده و یک مربع رسم می‌شود. حال متد <code>restore</code> فراخوانی می‌شود که آخرین حالت ذخیره‌شده را اعمال می‌کند، یعنی ویژگی <code>fillStyle</code> دوباره آبی می‌شود. حال یک مربع دیگر رسم می‌شود.</p>
<p>به عنوان یک نمونه‌ی بهتر، به کد زیر توجه کنید. در کد زیر به جای آنکه درگیر خنثی کردن تبدیلات قبلی (به‌ویژه چرخش) شویم، با استفاده از این دو متد به راحتی آن‌ها را به حالت قبل برمی‌گردانیم و به کار خود ادامه می‌دهیم. در کد زیر 16 مربع با زاویه‌های چرخش تصادفی به صورت یک جدول 4×4 رسم می‌شوند. کد زیر انعطاف‌پذیری مناسبی دارد و می‌توانید با استفاده از آن جدول‌های مربعی در اندازه‌های دیگر نیز ایجاد کنید:</p>
<pre><code class="language-javascript">
cvs.width = cvs.height = 500;

ctx.fillStyle = "#111";
ctx.fillRect(0, 0, cvs.width, cvs.height);

let grid = 4,
grid_unit = cvs.width / (grid * 2),

size = grid_unit * (grid * 2 / 10),
i, x, y;

ctx.save();

ctx.translate(grid_unit, grid_unit);
ctx.lineCap = ctx.lineJoin = "round";

for (i = 0, l = grid ** 2; i &lt; l; i++) {
    x = (i % grid) * grid_unit * 2;
    y = (i / grid | 0) * grid_unit * 2;

    ctx.save();

    ctx.translate(x, y);
    ctx.rotate(Math.random() * Math.PI * 2);

    ctx.lineWidth = (Math.random() * grid | 0) + grid;
    ctx.strokeStyle = "hsl(" + (Math.random() * 360 | 0) + ", 100%, 50%)";

    ctx.strokeRect(-size, -size, size * 2, size * 2);
    ctx.restore();
}

ctx.restore();
</code></pre>
<p>این کد نیاز به کمی توضیح دارد. ابتدا ویژگی‌های ابتدایی تنظیم می‌شوند. سپس سه متغیر <code>grid</code>، <code>grid_unit</code>، و <code>size</code> تعریف می‌شوند. متغیر <code>grid</code> اندازه‌ی جدول را تعیین می‌کند، متغیر <code>grid_unit</code> تعیین‌کننده‌ی فاصله‌ی هر عنصر جدول از دیگر عناصر است، و متغیر <code>size</code> نصف اندازه‌ی هر مربع است.</p>
<p>حال به کمک متد <code>save</code> حالت فعلی canvas ذخیره می‌شود تا بعدا استفاده شود. حال یک حلقه به اندازه‌ی توان دوم <code>grid</code> ایجاد می‌شود، زیرا تعداد اعضای جدول همین تعداد است. درون حلقه، متغیر‌های <code>x</code> و <code>y</code> تعیین می‌شوند. مقدار <code>i % grid</code> مقداری کوچکتر از <code>grid</code> است که می‌تواند مختصات افقی عنصر جدول را به ما بدهد، البته پیش از آن باید در مقدار مناسب ضرب شود. مقدار <code>i / grid | 0</code> نیز عددی صحیح است که می‌تواند مختصات عمودی عنصر جدول را به ما بدهد.</p>
<p>هنگام تبدیل یک آرایه‌ی یک‌بعدی به دو‌بعدی یا برعکس، از این فرمول‌ها استفاده می‌شود. در پروژه‌های آینده توضیحات بیشتری در این باره خواهیم داد. فعلا چیزهای مهم‌تری برای یادگیری هست پس ادامه می‌دهیم! حال که مختصات مرکز عنصر جدول را داریم، می‌توانیم یک مربع با رنگ، اندازه‌ی حاشیه، و چرخش تصادفی ایجاد کنیم. برای این کار ابتدا حالت فعلی canvas را ذخیره می‌کنیم تا برای گام بعدی حلقه از آن استفاده کنیم. حال مبدا مختصات را به مختصات <code>(x,y)</code> منتقل می‌کنیم و آن را با یک زاویه‌ی تصادفی می‌چرخانیم.</p>
<p>حال اندازه‌ی حاشیه را به صورت تصادفی (وابسته به <code>grid</code>) تعیین کرده و یک رنگ تصادفی به فرمت HSL ایجاد می‌کنیم. کد مربوط به <code>strokeStyle</code> برای ایجاد رنگ تصادفی کاربرد فراوانی دارد، هرچند رنگ‌های آن محدود هستند، اما ساده‌ترین روش برای این کار به شمار می‌رود. حال یک مربع به مرکز مبدا مختصات رسم می‌کنیم. برای این کار مختصات آن باید در <code>(-size,-size)</code> و طول ضلع آن <code>size * 2</code> باشد. حال که کار رسم تمام شد، تنظیمات قبلی canvas (پیش از چرخش و انتقال) را اعمال می‌کنیم.</p>
<p>پس از اجرای حلقه و اتمام ترسیمات، می‌توانیم برای ادامه‌ی ترسیمات باز هم متد <code>restore</code> را فراخوانی کنیم تا تنظیمات canvas به حالت اولیه (که پیش از آغاز حلقه ذخیره کردیم) باز گردد. البته این مورد، در صورتی که ترسیمات بیشتری انجام نمی‌شود، ضرورتی ندارد. همانطور که می‌بینید، توانستیم به کمک این دو متد، خود را از محاسبات پیچیده نجات دهیم!</p>
<h2>شکل فعلی</h2>
<p>نکته‌ی مهمی که باید به آن توجه کنید، این است که این دو متد، شکل فعلی را ذخیره و بازیابی نمی‌کنند. در واقع اگر بیشتر به این موضوع فکر کنیم، متوجه می‌شویم که این دو متد «نباید» شکل فعلی را تغییر دهند، زیرا در آن صورت محدودیت بیشتری ایجاد می‌شود و مشکلات این دو متد، از کاربردشان بیشتر می‌شود.</p>
<h2>ترتیب بازیابی اطلاعات</h2>
<p>همانطور که گفتیم، با هر بار فراخوانی متد <code>save</code>، یک آرایه از اطلاعات فعلی canvas ذخیره می‌شود. نکته‌ی مهم این است که این اطلاعات به جای یکدیگر قرار نمی‌گیرند، بلکه روی هم انباشته می‌شوند. و با هر بار اجرای متد <code>restore</code>، آخرین آرایه‌ی ذخیره‌شده استفاده می‌شود و پس از استفاده حذف می‌شود. برای درک بهتر موضوع به کد زیر دقت کنید. در کد زیر اطلاعات canvas در چند مرحله‌ی مختلف ذخیره می‌شوند و هنگام نیاز بازیابی می‌شوند:</p>
<pre><code class="language-javascript">
cvs.width = cvs.height = 500;

let rect = ctx.fillRect.bind(ctx, -75, -75, 200, 200);

ctx.fillStyle = "#111";
ctx.save(); /* STATE 1, BLACK */

ctx.fillStyle = "#0AF";
ctx.translate(110, 110);
ctx.save(); /* STATE 2, BLUE */

ctx.fillStyle = "#F30";
ctx.translate(230, 0);
ctx.save(); /* STATE 3, RED */

ctx.fillStyle = "#3D3";
ctx.translate(-230, 230);
ctx.save(); /* STATE 4, GREEN */

ctx.fillStyle = "#DD0";
ctx.translate(230, 0);
ctx.save(); /* STATE 5, YELLOW */

/* DRAW RECTS */
ctx.restore(); /* STATE 5, YELLOW */
rect();

ctx.restore(); /* STATE 4, GREEN */
rect();

ctx.restore(); /* STATE 3, RED */
rect();

ctx.restore(); /* STATE 2, BLUE */
rect();

ctx.restore(); /* STATE 1, BLACK */
ctx.save();

ctx.globalCompositeOperation = "destination-over";
ctx.fillRect(0, 0, cvs.width, cvs.height);

ctx.restore();
</code></pre>
<p>نتیجه‌ی کد بالا چهار مربع در رنگ‌های مختلف، با یک پس‌زمینه‌ی سیاه است. همانطور که در کد بالا می‌بینید، ابتدا پنج بار اطلاعات canvas ذخیره می‌شوند و سپس از هر کدام به ترتیب استفاده می‌شود. ممکن است برخی فکر کنند «حتما لازم است تمام اطلاعات ذخیره‌شده بازیابی شوند تا بتوانیم به ذخیره‌ی مجدد اطلاعات بپردازیم» اما اشتباه است. در هر زمان می‌توان اطلاعات canvas را ذخیره یا بازیابی کرد، اما توجه داشته باشید که اطلاعات زیادی را روی هم انباشته نکنید، زیرا ممکن است روی سرعت برنامه اثر بگذارد.</p>
<p>نکته‌ی پایانی اینکه اگر می‌خواهید یک آرایه پس از بازیابی حذف نشود و همچنان در حافظه باقی بماند، می‌توانید بلافاصله پس از فراخوانی متد <code>restore</code>، متد <code>save</code> را فرابخوانید تا دوباره همان اطلاعات وارد حافظه بشوند. شبیه به کاری که پیش از رسم مربع سیاه در کد بالا انجام شد.</p>
<h2>بررسی یک نمونه</h2>
<p>به عنوان یک نمونه‌ی نسبتا بزرگ و پیچیده، کد زیر می‌تواند به‌خوبی کاربرد‌های مختلف این دو متد را نشان دهد. در کد زیر سه شکل تصادفی ایجاد می‌شوند، سپس با متد <code>clip</code> ترسیمات به این شکل‌ها محدود می‌شوند، سپس یک افکت روی تصویری که درون این شکل‌ها رسم می‌شود اعمال شده، سپس به کمک متد <code>restore</code>، محدوده‌ی ذخیره‌شده‌ی قبلی اعمال شده و شکل‌های بعدی نیز به همین ترتیب این کار را انجام می‌دهند. در نهایت تصویر کامل پشت هرکدام از ترسیمات رسم می‌شود. متغیر <code>img</code> یک تصویر است که می‌توانید آن را به دلخواه تعیین کنید:</p>
<pre><code class="language-javascript">
let img = document.getElementById("an_image");

cvs.width = 1500;
cvs.height = 500;

/* (1) */
ctx.lineCap = ctx.lineJoin = "round";
ctx.lineWidth = 12;
ctx.strokeStyle = "#FFF";

/* (2) */
function clear (context) {
    context.save();
    
    context.globalCompositeOperation = "copy";
    context.fillStyle = "rgba(0, 0, 0, 0)";
    
    context.fillRect(0, 0, 1, 1);
    context.restore();
}

/* (3) */
function get_random (min, max) {
    return Math.random() * (max - min + 1) + min | 0;
}

/* (4) */
function generate_shape (n, min, max, rx, ry) {
    /* (5) */
    let i, x, y, r,
        
        angle = Math.PI * 2 / n,
        vertices = [];
    
    /* (6) */
    for (i = 0; i &lt; n; i++) {
        r = get_random(min, max);
        
        x = rx + r * Math.cos(i * angle);
        y = ry + r * Math.sin(i * angle);
        
        vertices.push(x, y);
    }
    
    /* (7) */
    return vertices;
}

/* (8) */
function apply_shape (vertices) {
    /* (9) */
    ctx.beginPath();
    
    /* (10) */
    for(let i = 0, l = vertices.length; i &lt; l; i += 2) {
        ctx.lineTo(vertices[i], vertices[i+1]);
    }
    
    ctx.closePath();
    
    /* (11) */
    ctx.stroke();
}

/* (12) */
let red_shade = generate_shape(70, 140, 180, 250, 250),
    gray_shade = generate_shape(70, 140, 180, 750, 250),
    negative_shade = generate_shape(70, 140, 180, 1250, 250);

/* (13) */
function draw_flowers () {
    /* (14) */
    clear(ctx);
    ctx.save();
    
    /* (15) */
    /* RED SHADE */
    
    apply_shape(red_shade);
    ctx.clip();
    
    ctx.drawImage(img, 0, 0, 500, 500);
    ctx.globalCompositeOperation = "color";
    
    ctx.fillStyle = "rgba(255, 0, 0, 0.5)";
    ctx.fillRect(0, 0, 500, 500);
    
    ctx.restore();
    ctx.save();
    
    /* (16) */
    /* GRAY SHADE */
    
    apply_shape(gray_shade);
    ctx.clip();
    
    ctx.drawImage(img, 500, 0, 500, 500);
    ctx.globalCompositeOperation = "color";
    
    ctx.fillStyle = "#000";
    ctx.fillRect(500, 0, 500, 500);
    
    ctx.restore();
    ctx.save();
    
    /* (17) */
    /* NEGATIVE SHADE */
    
    apply_shape(negative_shade);
    ctx.clip();
    
    ctx.drawImage(img, 1000, 0, 500, 500);
    ctx.globalCompositeOperation = "difference";
    
    ctx.fillStyle = "#BBB";
    ctx.fillRect(1000, 0, 500, 500);
    
    ctx.restore();
    ctx.save();
    
    /* (18) */
    /* NORMAL BACKGROUND */
    
    ctx.globalCompositeOperation = "destination-over";
    
    ctx.drawImage(img, 0, 0, 500, 500);
    ctx.drawImage(img, 500, 0, 500, 500);
    ctx.drawImage(img, 1000, 0, 500, 500);
    
    ctx.restore();
}

/* (19) */
onload = draw_flowers;
</code></pre>
<p><img decoding="async" src="https://css-tricks.ir/wp-content/uploads/2021/09/canvas-save-restore-clip.png" alt="canvas composition and clip" /><br />
در کد بالا از روش‌های خاصی استفاده شده که کاربرد زیادی در ترسیمات، به‌ویژه ترسیمات تصادفی و پویا دارند و یادگیری آن‌ها ضروری است. کد بالا را بررسی کرده و توضیحات مربوط به هر بخش را بخوانید. سپس سعی کنید بنا به چیزی که آموخته‌اید آن را تغییر دهید.</p>
<h3>بخش 1 تعریف موارد ابتدایی</h3>
<p>در این بخش ویژگی‌های <code>lineCap</code>، <code>lineJoin</code>، <code>strokeStyle</code>، و <code>lineWidth</code> تعیین می‌شوند. این ویژگی‌ها قرار نیست در طول برنامه تغییر کنند به همین دلیل در اینجا آن‌ها را تعیین می‌کنیم تا هنگام رسم راحت باشیم.</p>
<h3>بخش 2 تعریف جدید تابع clear</h3>
<p>این تابع همان کاربرد پیشین را دارد و کار آن پاک کردن لایه‌ی ترسیمات زمینه‌ی ورودی آن است. این تابع در این برنامه کاربردی ندارد و می‌توانید آن را حذف کنید. فقط از این جهت به این برنامه اضافه شده تا بتوانید کاربرد متد‌های <code>save</code> و <code>restore</code> را درون آن ببینید. به کمک این دو متد، دیگر نیازی نیست که مقدار‌های پیشین <code>globalCompositeOperation</code> و <code>fillStyle</code> ذخیره شوند.</p>
<h3>بخش 3 تابع تولید عدد تصادفی</h3>
<p>این تابع دارای یک کد ساده برای ایجاد اعداد تصادفی بین دو ورودی خود است و نیاز به توضیح خاصی ندارد. تنها نکته‌ی مهم در این تابع، استفاده از عملگر <code>|</code> همراه با صفر است. اگر این عملگر به این صورت با صفر به کار برود، قسمت اعشاری عدد سمت چپ را حذف می‌کند و می‌تواند کار متد <code>Math.floor</code> را سریع‌تر انجام دهد. هرچند این عملگر با اعداد منفی رفتار کمی متفاوتی دارد اما در اینجا می‌توان بدون مشکل از آن استفاده کرد.</p>
<h3>بخش 4 تابع ایجاد شکل تصادفی</h3>
<p>این تابع وظیفه‌ی ایجاد یک شکل تصادفی را بر عهده دارد. این تابع پنج ورودی نیاز دارد که به ترتیب تعداد اضلاع شکل <code>n</code>، کران پایین اعداد تصادفی <code>min</code>، کران بالای اعداد تصادفی <code>max</code>، و مختصات مرکز شکل <code>(rx,ry)</code> است. به جای مختصات مرکز می‌توان از متد <code>translate</code> کمک گرفت. این تابع از مختصات قطبی برای ایجاد شکل استفاده می‌کند که برتری‌های خاص خود را دارد که در ادامه به آن‌ها خواهیم پرداخت.</p>
<h3>بخش 5 متغیر‌های اولیه‌ی تابع</h3>
<p>در این بخش متغیر‌هایی که درون حلقه استفاده می‌شوند، و دو متغیر <code>vertices</code> و <code>angle</code> تعریف می‌شوند. متغیر <code>vertices</code> یک آرایه است که قرار است مختصات نقاط شکل را داشته باشد، و متغیر <code>angle</code> نیز نسبت 2π به <code>n</code> است. اگر با مختصات قطبی آشنا باشید، به‌خوبی کاربرد این متغیر را می‌دانید. این متغیر برای یافتن زاویه‌ی درست مختصات در حلقه استفاده شود.</p>
<h3>بخش 6 حلقه‌ی ایجاد نقاط</h3>
<p>حال می‌خواهیم مختصات شکل را ایجاد کنیم. برای این کار یک حلقه به طول <code>n</code> ایجاد می‌کنیم. درون این حلقه یک مقدار تصادفی برای <code>r</code> ایجاد می‌کنیم. این متغیر حکم شعاع در مختصات قطبی را دارد. سپس به کمک این متغیر و زاویه‌ی فعلی <code>i*angle</code> مختصات دکارتی (یا همان X و Y) نقطه را به دست می‌آوریم. فرمول استفاده شده برای محاسبه‌ی این مقدار، در حال حاضر، نیاز به توضیح ندارد اما بعدا به آن خواهیم پرداخت. حال این مختصات <code>(x,y)</code> را در آرایه‌ی <code>vertices</code> ذخیره می‌کنیم.</p>
<h3>بخش 7 مقدار خروجی تابع</h3>
<p>حال که تمام نقاط در متغیر <code>vertices</code> ذخیره شدند، باید این آرایه را به عنوان مقدار بازگشتی تابع تعیین کنیم. پس از اجرای تابع، یک آرایه از نقاط ایجاد‌شده برگدانده می‌شود.</p>
<h3>بخش 8 تابع رسم شکل</h3>
<p>این تابع تقریبا مشابه کدی است که پیش از این بررسی کرده‌ایم. این تابع یک ورودی از نوع آرایه شامل نقاط دریافت می‌کند و شکل مورد‌نظر را در شکل فعلی قرار می‌دهد. هرچند روش کار آن پیش ار این بررسی شده اما یک بار دیگر به آن می‌پردازیم.</p>
<h3>بخش 9 پاک کردن شکل فعلی پیشین</h3>
<p>این تابع ابتدا باید شکل فعلی قبلی را پاک کند. این بخش مهم است زیرا اولا روند برنامه به این صورت است، دوما اگر شکل فعلی بیش از حد بزرگ شود می‌تواند سرعت برنامه را پایین بیاورد.</p>
<h3>بخش 10 رسم نقاط</h3>
<p>در این بخش یک حلقه با گام 2 اجرا می‌شود که نقاط را به کمک متد <code>lineTo</code> رسم می‌کند. شاید این مورد به ذهن بیاید که «از آنجایی که شکل فعلی همین الان پاک شده، پس مختصات فعلی قلم تعریف‌نشده است و خط اول رسم نمی‌شود!» در واقع این کار از عمد انجام شده، زیرا قرار نیست خطی به نقطه‌ی اول رسم شود، بلکه فعلا باید قلم به آن نقطه برود. می‌توانستیم از تابع <code>moveTo</code> به صورت دستی استفاده کنیم ولی از تعریف‌نشده بودن مختصات قلم به نفع خودمان استفاده کردیم و یک کد کوتاه‌تر با همان کارکرد نوشتیم! در پایان حلقه، مختصات نهایی قلم روی نقطه‌ی آخر است، حال به کمک متد <code>closePath</code> نقطه‌ی آخر را به نقطه‌ی اول وصل می‌کنیم.</p>
<h3>بخش 11 رسم شکل ایجاد‌شده</h3>
<p>این بخش اختیاری است اما جلوه‌ی خوبی به برنامه می‌دهد. شکل فعلی پس از تعریف شدن به کمک متد <code>stroke</code> رسم می‌شود. از آنجایی که در بخش 1 ویژگی‌های موردنیاز این متد تعیین شده‌اند، در اینجا نیازی به تعیین چیزی نیست، این مورد از تکرار کد جلوگیری کرده و سرعت برنامه را نیز افزایش می‌دهد. سعی کنید در موارد مشابه چنین کاری انجام دهید.</p>
<h3>بخش 12 ایجاد سه شکل</h3>
<p>در این بخش سه شکل متفاوت به کمک تابع <code>generate_shape</code> ایجاد می‌شوند. ورودی‌های این سه تابع یکسان هستند اما می‌توانید کران‌ها و تعداد اضلاع هرکدام را به دلخواه تغییر دهید. البته بهتر است تغییری در مختصات مرکز ایجاد نکنید. از این سه متغیر، که آرایه‌ای از مختصات نقاط ایجاد‌شده را ذخیره کرده‌اند، در روند برنامه استفاده خواهد شد.</p>
<h3>بخش 13 تابع اصلی برنامه</h3>
<p>این تابع هسته‌ی اصلی برنامه است و تمام ترسیمات درون این تابع انجام می‌شود. در ادامه به هر بخش درون این تابع می‌پردازیم.</p>
<h3>بخش 14 پاک کردن ترسیمات و ذخیره‌ی اطلاعات فعلی</h3>
<p>در ابتدای تابع لایه‌ی ترسیمات پاک شده و اطلاعات فعلی آن ذخیره می‌شوند. پاک کردن لایه‌ی ترسیمات ضروری نیست و می‌توانید این کد و تابع <code>clear</code> را از برنامه حذف کنید، اما اجرای متد <code>save</code> ضروری است، زیرا در ادامه قرار است از متد <code>clip</code> استفاده شود و اطلاعات فعلی برای بازیابی لازم هستند.</p>
<h3>بخش 15 رسم شکل اول</h3>
<p>در این بخش شکل درون متغیر <code>red_shade</code> درون شکل فعلی قرار می‌گیرد، سپس با متد <code>clip</code> ترسیمات به این شکل محدود می‌شوند. سپس به کمک ویژگی <code>globalCompositeOperation</code> یک افکت روی این تصویر اعمال می‌شود و تصویر درون این محدوده رسم می‌شود.</p>
<h3>بخش 16 و 17 افکت‌های متفاوت</h3>
<p>پیش از اجرای هرکدام از این دو بخش، متد <code>restore</code> فراخوانی شده و محدوده به حالت قبل بازمی‌گردد، سپس این محدوده دوباره ذخیره می‌شود تا بعدا بازیابی شود. حال در هر بخش، هرکدام از شکل‌ها وارد شکل فعلی شده و یک تصویر با یک افکت متفاوت درون این محدوده رسم می‌شود. روند کار در این دو بخش نیز مانند بخش قبل است.</p>
<h3>بخش 18 رسم تصاویر اصلی</h3>
<p>در این بخش برای کامل کردن شکل و نمایش بهتر، تصاویر اصلی (بدون وجود محدوده) پشت تصاویر دارای افکت رسم می‌شوند. البته می‌توانستیم بدون نیاز به ویژگی <code>globalCompositeOperation</code> ابتدا تصاویر اصلی را رسم کنیم و سپس به رسم تصاویر دارای افکت بپردازیم.</p>
<h3>بخش 19 ایجاد مدیریت رویداد</h3>
<p>معمولا بدون نیاز به مدیریت رویداد می‌توان ترسیمات را انجام داد، اما در این مورد یک تصویر در صفحه وجود دارد و باید ابتدا منتظر بارگیری آن بمانیم. بنابراین یک مدیریت رویداد برای بارگیری (load) صفحه ایجاد می‌کنیم تا پس از بارگیری، تابع <code>draw_flowers</code> را اجرا کند.</p>
<h2>مختصات قطبی</h2>
<p>در این بخش شاید برای اولین بار با یک سامانه‌ی مختصاتی جدید آشنا شدید! مختصات قطبی در کنار مختصات دکارتی، یکی از معروف‌ترین سامانه‌های مختصاتی است که کاربرد فراوانی نیز دارد و در مقایسه با مختصات دکارتی برتری‌ها و ضعف‌هایی دارد. مختصات دکارتی همان مختصات عادی است که در ترسیمات canvas استفاده می‌شود.</p>
<p>در این بخش به روش کار این مختصات نمی‌پردازیم، بلکه چند نمونه بررسی می‌کنیم که برتری این مختصات را در ایجاد شکل‌های تصادفی نشان می‌دهد. در آموزش‌های آینده بیشتر و بهتر به روش کار این سامانه‌ی مختصاتی و کاربرد‌های بیشتری از آن خواهیم پرداخت. به نمونه کد زیر توجه کنید. در کد زیر برای سامانه‌های مختصاتی دکارتی و قطبی یک تابع ایجاد شکل تصادفی نوشته شده و از هرکدام برای ایجاد یک شکل با 50 نقطه استفاده می‌شود:</p>
<pre><code class="language-javascript">
cvs.width = 1000;
cvs.height = 500;

function get_random (min, max) {
    return Math.random() * (max - min + 1) + min | 0;
}

function apply_shape (vertices) {
    ctx.beginPath();
    
    for(let i = 0, l = vertices.length; i &lt; l; i += 2) {
        ctx.lineTo(vertices[i], vertices[i+1]);
    }
    
    ctx.closePath();
    ctx.stroke();
}

function generate_polar (n, min, max) {
    let i, x, y, r,
        
        angle = Math.PI * 2 / n,
        vertices = [];
    
    for (i = 0; i &lt; n; i++) {
        r = get_random(min, max);
        
        x = r * Math.cos(i * angle);
        y = r * Math.sin(i * angle);
        
        vertices.push(x, y);
    }
    
    return vertices;
}

function generate_cartesian (n, min, max) {
    let i, x, y,
        vertices = [];
    
    for (i = 0; i &lt; n; i++) {
        x = get_random(min, max);
        y = get_random(min, max);
        
        vertices.push(x, y);
    }
    
    return vertices;
}

ctx.fillStyle = "#111";
ctx.fillRect(0, 0, cvs.width, cvs.height);

let polar = generate_polar(50, 120, 250),
    cartesian = generate_cartesian(50, 0, 500);

ctx.lineCap = ctx.lineJoin = "round";
ctx.lineWidth = 7;

/* DRAW POLAR */
ctx.save();
ctx.translate(250, 250);
apply_shape(polar);

ctx.strokeStyle = "#3D3";
ctx.stroke();

ctx.restore();
ctx.beginPath();
ctx.save();

/* DRAW POLAR */
ctx.translate(500, 0);
apply_shape(cartesian);

ctx.strokeStyle = "#0AF";
ctx.stroke();

ctx.restore();
</code></pre>
<p><img decoding="async" src="https://css-tricks.ir/wp-content/uploads/2021/09/canvas-polar-coordination.png" alt="canvas polar vs. cartesian coordinates" /></p>
<p>در کد بالا، شکل ایجاد‌شده با مختصات قطبی به رنگ سبز، و شکل ایجاد‌شده با مختصات دکارتی با رنگ آبی رسم شده. همانطور که می‌بینید مختصات قطبی در ایجاد شکل‌های تصادفی بسیار بهتر عمل می‌کند. لازم به گفتن است که به دلیل تفاوت‌های اساسی این دو سامانه‌ی مختصاتی، لازم بود تغییرات کوچکی در کران‌های دو شکل ایجاد شود که البته مسئله‌ی مهمی نیست؛ اگر کد را تغییر دهید به‌خوبی متوجه خواهید شد!</p>
<h2>نتیجه‌گیری</h2>
<p>در این بخش سعی بر آن بود که به بهترین شکل کاربرد‌های گسترده‌ی متد‌های <code>save</code> و <code>restore</code> بررسی شوند. خوشبختانه نیاز به توضیحات زیادی در این باره نبود و توانستیم یک نمونه‌ی نسبتا پیچیده همراه با مفاهیمی تازه نیز بیاموزیم. در بخش بعدی بالاخره به انیمیشن می‌پردازیم و توابع و ترفند‌های مناسب کار با آن‌ها را بررسی می‌کنیم.</p>
<p>نوشته <a rel="nofollow" href="https://css-tricks.ir/tutorial/%d9%85%d8%aa%d8%af%e2%80%8c%d9%87%d8%a7%db%8c-save-%d9%88-restore-%d8%af%d8%b1-canvas/">متد‌های save و restore در canvas</a> اولین بار در <a rel="nofollow" href="https://css-tricks.ir">آموزش طراحی وب</a> پدیدار شد.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.ir/tutorial/%d9%85%d8%aa%d8%af%e2%80%8c%d9%87%d8%a7%db%8c-save-%d9%88-restore-%d8%af%d8%b1-canvas/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>تبدیلات transform در canvas</title>
		<link>https://css-tricks.ir/tutorial/%d8%aa%d8%a8%d8%af%db%8c%d9%84%d8%a7%d8%aa-transform-%d8%af%d8%b1-canvas/</link>
					<comments>https://css-tricks.ir/tutorial/%d8%aa%d8%a8%d8%af%db%8c%d9%84%d8%a7%d8%aa-transform-%d8%af%d8%b1-canvas/#respond</comments>
		
		<dc:creator><![CDATA[حسین رفیعی]]></dc:creator>
		<pubDate>Sat, 16 Oct 2021 14:48:44 +0000</pubDate>
				<category><![CDATA[آموزش]]></category>
		<category><![CDATA[canvas]]></category>
		<category><![CDATA[rotate]]></category>
		<category><![CDATA[transform]]></category>
		<category><![CDATA[translate]]></category>
		<guid isPermaLink="false">https://css-tricks.ir/?p=9421</guid>

					<description><![CDATA[<p>در این بخش به مفاهیم پایه تبدیلات transform، کاربرد‌های گسترده‌ی آن‌ها، و متد‌های مرتبط با آن می‌پردازیم.</p>
<p>نوشته <a rel="nofollow" href="https://css-tricks.ir/tutorial/%d8%aa%d8%a8%d8%af%db%8c%d9%84%d8%a7%d8%aa-transform-%d8%af%d8%b1-canvas/">تبدیلات transform در canvas</a> اولین بار در <a rel="nofollow" href="https://css-tricks.ir">آموزش طراحی وب</a> پدیدار شد.</p>
]]></description>
										<content:encoded><![CDATA[<p>در این بخش به بررسی انواع تبدیل یا transform در canvas می‌پردازیم. این توابع کاربرد فراوانی داشته و اگر به درستی استفاده شوند، می‌توانند ترسیمات بسیار پیچیده را در چند خط کد خلاصه کنند. موقعیت‌هایی پیش می‌آید که یک شکل پیچیده ایجاد کرده اید اما می‌خواهید اندازه‌ی آن را تغییر دهید، یا اینکه در محل درستی از صفحه نیست. شاید در ترسیمات شما مبدا مختصات در مرکز صفحه قرار دارد و رسم آن شکل در مختصات پیش‌فرض canvas ممکن نیست.</p>
<p>برای حل این مشکلات می‌توانید از تبدیل‌ها یا متد‌های transform استفاده کنید. مشکل اینجاست که پیش از استفاده از این توابع باید با آن‌ها آشنایی داشته و نکاتی را در ذهن داشته باشید تا به دردسر نیوفتید. تبدیل‌ها در canvas سه متد اصلی دارند که جداگانه به هرکدام می‌پردازیم. به جز این سه متد، سه متد دیگر نیز هستند که در ادامه به آن‌ها خواهیم پرداخت.</p>
<h2>تغییر اندازه (scale)</h2>
<p>با استفاده از متد <code>scale</code> می‌توان شکل‌ها را تغییر اندازه داد. این متد دو ورودی می‌پذیرد که به ترتیب تغییر اندازه در راستای محور X‌ها و تغییر اندازه در راستای محور Yها هستند. نکته‌ی مهم اینکه این ورودی‌ها می‌توانند اعداد منفی یا صفر نیز باشند! اگر یکی از ورودی‌ها صفر باشد، مختصات در آن راستا صفر برابر می‌شوند و دیگر در canvas چیزی رسم نخواهد شد. اگر ورودی‌ها منفی باشند، جهت محور مربوطه قرینه می‌شود. یعنی اگر ورودی Y‌ها منفی باشد، جهت محور Y‌ها که از بالا به پایین است، قرینه شده و از پایین به بالا می‌شود.</p>
<pre><code class="language-javascript">
ctx.scale(scale_x. scale_y);
</code></pre>
<p>به نمونه کد زیر توجه کنید. در کد زیر با استفاده از این متد اندازه‌ی ترسیمات در راستای محور X‌ها 2 برابر می‌شود. به همین دلیل مربعی که تعریف کرده‌ایم به شکل مستطیل رسم می‌شود. کد را تغییر داده و تغییرات آن را مشاهده کنید:</p>
<pre><code class="language-javascript">
ctx.scale(2, 1);

ctx.fillStyle = "#0AF";
ctx.fillRect(50, 50, 100, 100);
</code></pre>
<p>در کد بالا یک مربع در مختصات (50,50) و به طول ضلع 100 تعریف شد، اما به خاطر تغییر اندازه، یک مستطیل در مختصات (100,50) و در ابعاد 200 × 100 رسم شد. نکته‌ی مهم اینکه می‌توان به تعداد دلخواه این متد را فراخوانی کرد، اما باید بدانید که ورودی‌های این متد در اندازه‌ی قبلی ضرب می‌شوند. یعنی در کد زیر ترسیمات نهایی سه‌برابر نمی‌شوند، بلکه شش برابر می‌شوند. به این دلیل که ورودی‌های متد دوم در متد اول ضرب می‌شوند:</p>
<pre><code class="language-javascript">
ctx.scale(2, 2);
ctx.scale(3, 3);

/* 6 TIMES LARGER */
</code></pre>
<h2>انتقال (translate)</h2>
<p>با استفاده از متد <code>translate</code> می‌توان مبدا مختصات در canvas را تغییر داد. این متد نیز مانند متد قبل دو ورودی می‌پذیرد که انتقال در راستای محور X‌ها و انتقال در راستای محور Y‌ها هستند. ورودی‌های این متد نیز می‌توانند هر نوع عددی باشند. اگر یکی از ورودی‌ها صفر باشد، مبدا در آن راستا تغییری نمی‌کند. اگر ورودی منفی باشد، مبدا در آن راستا رو به عقب می‌رود.</p>
<pre><code class="language-javascript">
ctx.translate(translate_x, translate_y);
</code></pre>
<p>به نمونه کد زیر توجه کنید. در کد زیر مبدا ترسیمات 200 واحد در هر راستا رو به جلو می‌روند و دایره‌ای که به مرکز مبدا مختصات رسم می‌شود، منتقل می‌شود. برای یادگیری بهتر کد را تغییر داده و تغییرات را مشاهده کنید:</p>
<pre><code class="language-javascript">
ctx.translate(200, 200);
ctx.arc(0, 0, 150, 0, Math.PI * 2);

ctx.fillStyle = "#CC0";
ctx.fill();
</code></pre>
<p>نمونه کد زیر مبدا مختصات را به مرکز عنصر آورده و راستای محور Y‌ها را نیز قرینه می‌کند. این کد هنگام رسم نمودار‌ها، به‌ویژه نمودار‌های ریاضی کاربردی خواهد بود. این کد را در ترسیمات خود قرار داده و تغییرات آن را مشاهده کنید:</p>
<pre><code class="language-javascript">
ctx.translate(cvs.width / 2, cvs.height / 2);
ctx.scale(1, -1);
</code></pre>
<p>همانند متد قبل، می‌توانید این متد را به تعداد دلخواه فراخوانی کنید، اما باید توجه داشته باشید که ورودی‌های این متد، با ورودی‌های پیشین جمع می‌شوند. این یعنی در کد زیر مبدا مختصات به جای (250,100) در (350,50) خواهد بود، زیرا ورودی‌های جدید، با ورودی‌های پیشین جمع می‌شوند:</p>
<pre><code class="language-javascript">
ctx.translate(100, -50);
ctx.translate(250, 100);

/* ORIGIN AT (350, 50) */
</code></pre>
<h2>چرخش (rotate)</h2>
<p>با استفاده از متد <code>rotate</code> می‌توان محور‌های مختصات را چرخاند. این متد یک ورودی می‌پذیرد که یک زاویه به رادیان است و محور‌های مختصات را در جهت عقربه‌های ساعت و به مرکز مبدا مختصات می‌چرخاند. این نوع چرخش با آنچه در CSS وجود دارد متفاوت است زیرا این چرخش به مکان مبدا مختصات بستگی دارد؛ ولی در CSS مرکز چرخش همیشه در مرکز عنصر قرار دارد.</p>
<pre><code class="language-javascript">
ctx.rotate(angle);
</code></pre>
<p>شبیه به متد‌های قبل، می‌توانید این متد را به تعداد دلخواه و با هر مقدار عددی به عنوان ورودی فراخوانی کنید، اما باید توجه داشته باشید که زاویه‌ی ورودی با زوایای قبلی جمع می‌شود. یعنی در کد زیر، چرخش نهایی 120 درجه است، نه 90 درجه!</p>
<pre><code class="language-javascript">
ctx.rotate(Math.PI / 6); /* 30 DEGREES */
ctx.rotate(Math.PI / 2); /* 90 DEGREES */

/* ROTATION IS 120 DEGREES */
</code></pre>
<p>نکته‌ی مهم دیگری که باید به آن توجه داشته باشید، این است که راستای محور‌ها در جهت چرخش اثرگذار است. برای نمونه کد زیر را بررسی کنید. در کد زیر ابتدا مبدا مختصات به مرکز عنصر رفته و یک مربع در مرکز آن رسم می‌شود. در هر فریم از انیمیشن، 0.05 رادیان به زاویه‌ی چرخش این مربع اضافه می‌شود و مربع رسم می‌شود:</p>
<pre><code class="language-javascript">
cvs.width = cvs.height = 500;

ctx.translate(cvs.width / 2, cvs.height / 2);
ctx.scale(1, 1); /* EXPERIMENT */

ctx.fillStyle = "#CC0";

function rotate () {
    ctx.clearRect(-cvs.width, -cvs.height, cvs.width * 2, cvs.height * 2);
    ctx.rotate(0.05);
    
    ctx.fillRect(-100, -100, 200, 200);
}

setInterval(rotate, 16);
</code></pre>
<p>در کد بالا متد scale با ورودی‌های 1 نیز نوشته شده. هر کدام از ورودی‌ها را به -1 تغییر دهید تا تاثیر آن را بر جهت چرخش مربع ببینید. همچنین می‌توانید مختصات مربع را تغییر دهید تا درک بهتری از مفهوم «مرکز چرخش» به دست آورید. هرچند در کد بالا از تابع setInerval برای ایجاد انیمیشن استفاده شد، اما این تابع برای این هدف مناسب نیست. در آموزش‌های آینده توابع مناسب‌تر بررسی خواهند شد.</p>
<h2>ماتریس تبدیلات</h2>
<p>حال باید کمی به ریاضیات بپردازیم. اگر میانه‌ی خوبی با آن ندارید، نگران نباشید زیرا موضوع ساده‌تر از آن است که به نظر می‌رسد. ماتریس یک آرایش از اعداد است و قوانین خاصی برای ضرب و جمع با دیگر ماتریس‌ها دارد. ماتریس‌ها در هندسه‌ی برداری کاربرد فراوانی دارند و آشنایی نسبی با آن‌ها ضروری است.</p>
<p>هرکدام از تبدیل‌هایی که بررسی شد، یک ماتریس ویژه‌ی خود دارند. درضمن ساختار ترسیمات canvas نیز یک ماتریس پیش‌فرض دارد. این ماتریس پیش‌فرض دارای انتقال صفر، چرخش  صفر، و تغییر اندازه‌ی 1 است، یعنی هیچ تبدیلی به ترسیمات اعمال نمی‌شود.</p>
<p>هنگام استفاده از یکی از متد‌های تبدیلات، یک ماتریس جدید تعریف می‌شود. این ماتریس اطلاعات مربوط به متد را درون خود دارد. مثلا اگر متد انتقال با ورودی (200,300) فراخوانی شود، یک ماتریس با این اطلاعات ایجاد می‌شود. سپس این ماتریس، در ماتریس اصلی canvas ضرب می‌شود. وقتی دو ماتریس تبدیلات در یکدیگر ضرب می‌شوند، ماتریس نتیجه، ترکیبی از هر دو تبدیل خواهد بود. برای درک بهتر به شبه‌کد زیر توجه کنید:</p>
<pre><code class="language-javascript">
{rotate: 45} × {scale: (2,3)} = { rotate: 45 , scale: (2,3) }
{scale: (2,3)} × {rotate: 45} = { scale: (2,3) , rotate: 45 }
</code></pre>
<p>توجه کنید که ترتیب مهم است. یعنی ضرب ماتریس translate در rotate، با ضرب ماتریس rotate در translate متفاوت است! این موضوع را پس از اجرای چند تبدیل پیچیده نیز متوجه خواهید شد. هنگامی که یک شکل در canvas تعریف می‌شود، مختصات آن شکل مستقیما در ماتریس اصلی canvas ضرب شده و تمام تبدیلات به آن شکل اعمال می‌شود.</p>
<h2>متد transform</h2>
<p>این متد این امکان را به ما می‌دهد که یک ماتریس تبدیلات را مستقیما در ماتریس اصلی canvas ضرب کنیم. این متد شش ورودی می‌پذیرد که اعضای ماتریس تبدیلات هستند. در زیر نام هر ورودی و نقش آن نوشته شده اما برای حفظ سادگی از توضیح آن‌ها خودداری می‌کنیم:</p>
<pre><code class="language-javascript">
ctx.transform(scale_x, skew_y, translate_x, scale_y, skew_x, translate_y);
</code></pre>
<p>این متد هنگامی کاربرد دارد که یک ماتریس تبدیلات داریم پس به جای آنکه سعی کنیم اطلاعات مختلف آن مانند زاویه‌ی چرخش و تغییر اندازه‌ها را از آن خارج کنیم، آن را مستقیما استفاده می‌کنیم.</p>
<h2>متد setTransform</h2>
<p>این متد درست مانند متد <code>transform</code> رفتار می‌کند، با این تفاوت که ورودی آن در ماتریس اصلی canvas ضرب نمی‌شود، بلکه به جای آن قرار می‌گیرد! یعنی با اجرای این متد، تمام تبدیلاتی که پیش از آن اجرا شده‌اند حذف شده و تبدیلات درون ماتریس ورودی این متد به جای آن‌ها قرار می‌گیرند.</p>
<p>این متد می‌تواند در از بین بردن تمام تبدیلات قبلی کاربردی باشد. کد زیر تمام تبدیلات اعمال‌شده به ماتریس اصلی canvas را حذف کرده و آن را به حالت اولیه باز می‌گرداند:</p>
<pre><code class="language-javascript">
ctx.setTransform(1, 0, 0, 1, 0, 0);
</code></pre>
<h2>متد resetTransform</h2>
<p>این متد که هنوز در مرحله‌ی آزمایشی قرار دارد، ماتریس اصلی canvas را به حالت اولیه باز می‌گرداند:</p>
<pre><code class="language-javascript">
// Reset transformation matrix to the identity matrix
ctx.resetTransform();
</code></pre>
<p>البته پشتیبانی از این متد به خوبی <code>setTransform</code> نیست و پیشنهاد می‌شود به جای این متد از <code>setTransform</code> استفاده کرده یا حداقل از کد زیر برای ایجاد پشتیبانی از این متد استفاده کنید:</p>
<pre><code class="language-javascript">
if (!CanvasRenderingContext2D.prototype.resetTransform) {
    CanvasRenderingContext2D.prototype.resetTransform = function () {
        this.setTransform(1, 0, 0, 1, 0, 0);
    }
}
</code></pre>
<h2>مکان مهم است!</h2>
<p>اگر به توضیحات ماتریس تبدیلات توجه کرده باشید، گفتیم هنگام تعریف شکل‌ها، مختصات ورودی شکل‌ها مستقیما در ماتریس تبدیلات canvas ضرب می‌شوند و به این ترتیب، تبدیلات تعریف‌شده روی آن‌ها اعمال می‌شوند. این مورد را به بیانی دیگر بازگو می‌کنیم: «اگر یک متد تبدیل، بعد از تعریف شکل فراخوانی شود، روی آن شکل تاثیری نخواهد داشت!» برای درک بهتر به کد زیر توجه کنید:</p>
<pre><code class="language-javascript">
ctx.rect(50, 50, 150, 150);

ctx.scale(2, 1);
ctx.rect(350, 50, 100, 100);

ctx.fillStyle = "#3D3";
ctx.fill();
</code></pre>
<p>با اجرای کد بالا متوجه می‌شوید که مربع اول از متد scale تاثیر نگرفته، زیرا پیش از آن متد تعریف شده. اگر این متد پیش از مربع اول فراخوانی می‌شد، روی هر دو شکل تاثیر می‌گذاشت.</p>
<h2>ترتیب مهم است!</h2>
<p>اگر به توضیحات ماتریس تبدیلات توجه کرده باشید، گفتیم ترتیب ضرب دو ماتریس مهم است. این موضوع نیاز به توضیح بیشتری دارد اما سعی می‌کنیم بر پایه‌ی تعریف تبدیلات پیشرفته و به مسائل مربوط به ماتریس‌ها اشاره‌ای نکنیم. هرچند توضیح این موضوع به کمک ماتریس‌ها بسیار ساده‌تر است!</p>
<p>برای درک بهتر باید یک آزمایش کوچک ترتیب بدهیم. در این آزمایش از یک مربع به عنوان شکل استفاده می‌کنیم اما لازم است مکان محور‌های مختصات را نیز بدانیم. بنابراین کد زیر به عنوان زیربنای این آزمایش به کار می‌رود:</p>
<pre><code class="language-javascript">
function clear (context) {
    context.globalCompositeOperation = "copy";
    context.fillStyle = "rgba(0, 0, 0, 0)";
    
    context.fillRect(0, 0, 1, 1);
    context.globalCompositeOperation = "source-over";
}

function draw_shape (color) {
    ctx.beginPath();
    
    ctx.moveTo(0, 0);
    ctx.lineTo(cvs.width, 0);
    
    ctx.moveTo(0, 0);
    ctx.lineTo(0, cvs.height);
    
    ctx.rect(50, 50, 100, 100);
    
    ctx.lineWidth = 7;
    ctx.strokeStyle = color;
    
    ctx.stroke();
    ctx.beginPath();
}
</code></pre>
<p>این تابع یک رنگ به عنوان ورودی دریافت کرده و محور‌های مختصات و یک مربع به مختصات و طول ضلع ثابت رسم می‌کند. از ترسیمات درون این تابع برای فهمیدن تاثیر تبدیلات پیچیده استفاده می‌کنیم.</p>
<p>در گام اول، کد را بدون هیچ تبدیلی اجرا می‌کنیم. برای گام اول می‌توان از رنگ سیاه استفاده کرد. دو تابع clear و draw_shape را به کد اصلی خود اضافه کرده و کد‌هایی که در ادامه بررسی می‌شوند را اجرا کنید.</p>
<h3>آزمایش اول، تبدیلات ساده</h3>
<p>در کد زیر چند تبدیل ساده اجرا می‌شوند و هرکدام به رنگ ویژه‌ی خود در canvas رسم می‌شوند. همانطور که گفتیم، رنگ سیاه ترسیمات بدون تبدیل هستند. کد زیر را اجرا کرده و نتیجه را ببینید:</p>
<pre><code class="language-javascript">
clear(ctx);
draw_shape("#111");

ctx.resetTransform();
ctx.rotate(Math.PI / 6); /* 30deg */
draw_shape("#F00"); /* RED */

ctx.resetTransform();
ctx.translate(150, 200);
draw_shape("#3D3"); /* GREEN */
</code></pre>
<h3>آزمایش دوم، انتقال و چرخش</h3>
<p>در کد زیر ابتدا متد انتقال و سپس متد چرخش اجرا شده و شکل به رنگ آبی رسم شده. سپس تبدیلات به حالت اولیه بازگشته. این بار ابتدا متد چرخش و سپس متد انتقال (با همان ورودی‌ها) اجرا شده و شکل به رنگ سبز رسم شده. کد زیر را اجرا کرده و تفاوت نتایج را ببینید. همانطور که می‌بینید، ترتیب اجرای تبدیلات می‌تواند نتایج متفاوتی ایجاد کند:</p>
<pre><code class="language-javascript">
clear(ctx);
draw_shape("#111");

ctx.translate(200, 100);
ctx.rotate(Math.PI / 6);
draw_shape("#0AF"); /* BLUE */

ctx.resetTransform();

ctx.rotate(Math.PI / 6);
ctx.translate(200, 100);
draw_shape("#3D3"); /* GREEN */
</code></pre>
<h2>کاربرد‌های ماتریس تبدیلات</h2>
<p>متاسفانه، با وجود کابرد‌های بسیار زیاد ماتریس تبدیلات، نمی‌توان در این آموزش به آن پرداخت. هم به دلیل اینکه مخاطب به تازگی با این موضوع آشنا شده، و هم اینکه ریاضیات بخش زیادی از آن‌ها را تشکیل می‌دهد و افراد زیادی از آن فراری هستند! مواردی که در این آموزش بررسی شدند بخش بسیار کوچکی از کاربرد‌های تبدیلات بودند. احتمالا در آینده‌ی نزدیک یک آموزش ویژه‌ی این مورد خواهیم داشت؛ البته در آن خبری از آموزش مسائل ابتدایی ریاضی نیست بلکه انتظار می‌رود خوانندگان آن مطلب تا حدودی با این مسائل آشنا باشند!</p>
<h2>بررسی یک نمونه</h2>
<p>در کد زیر سه نمونه چرخش شکل به صورت انیمیشن اجرا می‌شود. در این کد تا جای ممکن از متد‌های تبدیلات استفاده شده. توضیح خاصی درباره‌ی این کد ارائه نمی‌شود اما سعی کنید تا جایی که ممکن است آن را تغییر دهید تا درک بهتری از روش کار آن به دست بیاورید:</p>
<pre><code class="language-javascript">
cvs.width = 1500;
cvs.height = 500;

let rect_1 = {
        center: [250, 250],
        size: 130,
        
        angle: 0,
        angle_speed: 0.05,
        
        color: "#E30",
        
        update: function () {
            this.angle += this.angle_speed;
        }
    },
    
    rect_2 = {
        center: [750, 250],
        size: 75,
        
        angle: 0,
        angle_speed: 0.05,
        
        color: "#3D3",
        
        update: function () {
            this.angle += this.angle_speed;
        }
    },
    
    rect_3 = {
        center: [],
        size: 100,
        
        angle: 0,
        angle_speed: 0.03,
        
        rotation_center: [1250, 250],
        rotation_radius: 80,
        
        color: "#0AF",
        
        update: function () {
            this.angle += this.angle_speed;
            
            this.center[0] = this.rotation_center[0] + this.rotation_radius * Math.cos(this.angle);
            this.center[1] = this.rotation_center[1] + this.rotation_radius * Math.sin(this.angle);
        }
    };

ctx.lineWidth = 5;
ctx.lineCap = ctx.lineJoin = "round";

function draw_rotations () {
    ctx.resetTransform();
    ctx.clearRect(0, 0, 1500, 500);
    
    /* 1 */
    ctx.translate(...rect_1.center);
    
    ctx.rotate(rect_1.angle);
    ctx.strokeStyle = rect_1.color;
    
    ctx.strokeRect(-rect_1.size, -rect_1.size, rect_1.size * 2, rect_1.size * 2);
    rect_1.update();
    
    /* 2 */
    ctx.resetTransform();
    ctx.rotate(-rect_2.angle);
    
    ctx.scale(0.1, 0.1);
    ctx.translate(...rect_2.center);
    
    ctx.scale(10, 10);
    ctx.rotate(rect_2.angle);
    
    ctx.strokeStyle = rect_2.color;
    ctx.strokeRect(-rect_2.size + rect_2.center[0], -rect_2.size + rect_2.center[1], rect_2.size * 2, rect_2.size * 2);
    
    rect_2.update();
    
    /* 3 */
    rect_3.update();
    ctx.resetTransform();
    
    ctx.translate(...rect_3.center);
    ctx.rotate(-rect_3.angle * 2);
    
    ctx.strokeStyle = rect_3.color;
    ctx.strokeRect(-rect_3.size, -rect_3.size, rect_3.size * 2, rect_3.size * 2);
}

setInterval(draw_rotations, 16);
</code></pre>
<h2>نتیجه‌گیری</h2>
<p>در این آموزش سعی کردیم درک مناسبی از تبدیلات در canvas ایجاد کنیم. کاربرد تبدیلات در همه‌جا به چشم می‌خورد، مشکل اینجاست که برای استفاده‌ی بهتر از آن‌ها یا حتی درک روش کار آن‌ها، تسلط به ریاضیات لازم است. در دنباله‌ی این قسمت از آموزش بیشتر به ماتریس‌ها خواهیم پرداخت. در آموزش بعدی به سراغ متد‌های <code>save</code> و <code>restore</code> و کاربرد‌های گسترده‌ی آن‌ها می‌رویم.</p>
<p>نوشته <a rel="nofollow" href="https://css-tricks.ir/tutorial/%d8%aa%d8%a8%d8%af%db%8c%d9%84%d8%a7%d8%aa-transform-%d8%af%d8%b1-canvas/">تبدیلات transform در canvas</a> اولین بار در <a rel="nofollow" href="https://css-tricks.ir">آموزش طراحی وب</a> پدیدار شد.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.ir/tutorial/%d8%aa%d8%a8%d8%af%db%8c%d9%84%d8%a7%d8%aa-transform-%d8%af%d8%b1-canvas/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>تعامل شکل فعلی و لایه‌ی ترسیمات در canvas</title>
		<link>https://css-tricks.ir/tutorial/%d8%aa%d8%b9%d8%a7%d9%85%d9%84-%d8%b4%da%a9%d9%84-%d9%81%d8%b9%d9%84%db%8c-%d9%88-%d9%84%d8%a7%db%8c%d9%87%e2%80%8c%db%8c-%d8%aa%d8%b1%d8%b3%db%8c%d9%85%d8%a7%d8%aa-%d8%af%d8%b1-canvas/</link>
					<comments>https://css-tricks.ir/tutorial/%d8%aa%d8%b9%d8%a7%d9%85%d9%84-%d8%b4%da%a9%d9%84-%d9%81%d8%b9%d9%84%db%8c-%d9%88-%d9%84%d8%a7%db%8c%d9%87%e2%80%8c%db%8c-%d8%aa%d8%b1%d8%b3%db%8c%d9%85%d8%a7%d8%aa-%d8%af%d8%b1-canvas/#respond</comments>
		
		<dc:creator><![CDATA[حسین رفیعی]]></dc:creator>
		<pubDate>Sat, 09 Oct 2021 15:30:43 +0000</pubDate>
				<category><![CDATA[آموزش]]></category>
		<category><![CDATA[alpha]]></category>
		<category><![CDATA[canvas]]></category>
		<category><![CDATA[composite]]></category>
		<category><![CDATA[effect]]></category>
		<guid isPermaLink="false">https://css-tricks.ir/?p=9410</guid>

					<description><![CDATA[<p>در این آموزش به دو ویژگی globalAlpha و globalCompositeOperation و کاربرد‌های فراوان این ویژگی‌ها می‌پردازیم.</p>
<p>نوشته <a rel="nofollow" href="https://css-tricks.ir/tutorial/%d8%aa%d8%b9%d8%a7%d9%85%d9%84-%d8%b4%da%a9%d9%84-%d9%81%d8%b9%d9%84%db%8c-%d9%88-%d9%84%d8%a7%db%8c%d9%87%e2%80%8c%db%8c-%d8%aa%d8%b1%d8%b3%db%8c%d9%85%d8%a7%d8%aa-%d8%af%d8%b1-canvas/">تعامل شکل فعلی و لایه‌ی ترسیمات در canvas</a> اولین بار در <a rel="nofollow" href="https://css-tricks.ir">آموزش طراحی وب</a> پدیدار شد.</p>
]]></description>
										<content:encoded><![CDATA[<p>اگر از آموزش‌های پیش به یاد داشته باشید، گفتیم ویژگی‌هایی برای تغییر تعامل شکل فعلی و لایه‌ی ترسیمات وجود دارند که می‌توانند به کلی ترسیمات نهایی درون canvas را تغییر دهند. در این بخش به این ویژگی‌ها می‌پردازیم و برخی از کاربردهای مهم آن‌ها را بررسی می‌کنیم. پیش از آغاز، توجه کنید که شکل فعلی فقط زمانی با لایه‌ی ترسیمات تعامل خواهد داشت که بخواهد رسم شود. در غیر این صورت این ویژگی‌ها هیچ تاثیری نخواهند داشت.</p>
<h2>ویژگی globalAlpha</h2>
<p>این ویژگی یک شفافیت کلی برای تمام ترسیمات تعیین می‌کند. مقدار پیش‌فرض آن 1 (بدون شفافیت) است و همیشه عددی بین 0 و 1 می‌پذیرد. اگر عددی خارج از این بازه به آن بدهیم، مقدار آن به 1 برمی‌گردد. ممکن است با خود بگویید «با وجود فرمت‌های رنگ مانند <code>rgba</code> که درون خود شفافیت دارند، چه نیازی به این ویژگی است؟» پاسخ این است که مواردی مانند تصویر وجود دارند که نمی‌توان برای آن‌ها به کمک <code>rgba</code> شفافیت ایجاد کرد. در این موارد این ویژگی کاربرد دارد. به نمونه کد زیر دقت کنید:</p>
<pre><code class="language-javascript">
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);
</code></pre>
<p>در کد بالا تمام ترسیمات دارای 50% شفافیت خواهند بود. پس‌زمینه‌ی سیاه از این جهت اضافه شده که بتوانید بهتر شفافیت را تشخیص دهید. جدا از بحث ایجاد شفافیت برای تصاویر، این ویژگی کاربرد فوق‌العاده‌ی دیگری نیز دارد که برای آشنایی با آن ابتدا باید به ویژگی بعدی بپردازیم.</p>
<h2>ویژگی globalCompositeOperation</h2>
<p>این ویژگی شبیه به نام نسبتا طولانی که دارد، مقدار‌های زیادی نیز می‌پذیرد! این ویژگی نوع تعامل ترسیمات جدیدی که قرار است به لایه‌ی ترسیمات اضافه شوند را با ترسیمات قبلی مشخص می‌کند. این ویژگی مقادیر زیر را می‌پذیرد که به توضیح هر یک می‌پردازیم:</p>
<pre><code class="language-javascript">
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";
</code></pre>
<p>خوشبختانه لازم نیست تمام این مقدار‌ها را بشناسید، هرچند اگر بشناسید ابزار بسیار قدرتمندی در دست خواهید داشت! در این آموزش به توضیح مختصری از هرکدام خواهیم پرداخت و چند مورد مهم از آن‌ها را بررسی خواهیم کرد. برای راحتی کار و شناخت بهتر، در این مقادیر منظور از destination ترسیمات قبلی، و منظور از source ترسیمات جدید است.</p>
<ul>
<li><code>source-over</code> ترسیمات جدید روی ترسیمات قبلی رسم می‌شوند. (مقدار پیش‌فرض)</li>
<li><code>source-in</code> بخش‌هایی از ترسیمات جدید باقی می‌مانند که روی ترسیمات قبلی باشند.</li>
<li><code>source-out</code> بخش‌هایی از ترسیمات جدید باقی می‌مانند که روی ترسیمات قبلی نباشند.</li>
<li><code>source-atop</code> شبیه به source-in ولی ترسیمات دیگر حذف نمی‌شوند.</li>
<li><code>destination-over</code> ترسیمات قبلی روی ترسیمات جدید قرار می‌گیرند.</li>
<li><code>destination-in</code> بخش‌هایی از ترسیمات قبلی باقی می‌مانند که روی ترسیمات جدید قرار می‌گیرند.</li>
<li><code>destination-out</code> بخش‌هایی از ترسیمات قبلی باقی می‌مانند که روی ترسیمات جدید قرار نمی‌گیرند.</li>
<li><code>destination-atop</code> فقط بخش‌هایی از ترسیمات قبلی باقی می‌مانند که روی ترسیمات جدید قرار می‌گیرند؛ و ترسیمات جدید نیز پشت آن‌ها رسم می‌شوند.</li>
<li><code>copy</code> فقط ترسیمات جدید باقی می‌مانند. دیگر ترسیمات حذف می‌شوند.</li>
<li><code>xor</code> بخش‌هایی که ترسیمات قبلی و جدید روی هم قرار می‌گیرند حذف می‌شوند. دیگر بخش‌ها به صورت عادی رسم می‌شوند.</li>
<li><code>lighter</code> در بخش‌هایی که ترسیمات قبلی و جدید روی هم قرار می‌گیرند، مقدار‌های رنگ با هم جمع می‌شوند.</li>
<li><code>multiply</code> هر پیکسل از ترسیمات جدید با پیکسل ترسیمات قبلی ضرب می‌شود. نتیجه رنگ‌های تیره‌تر است.</li>
<li><code>screen</code> برعکس مقدار multiply نتیجه رنگ‌های روشن‌تر است.</li>
<li><code>overlay</code> ترکیبی از multiply و screen که در آن بخش‌های تیره، تیره‌تر و بخش‌های روشن، روشن‌تر می‌شوند.</li>
<li><code>darken</code> تیره‌ترین رنگ‌ها از هرکدام از ترسیمات باقی می‌مانند.</li>
<li><code>lighten</code> روشن‌ترین رنگ‌ها از هرکدام از ترسیمات باقی می‌مانند.</li>
<li><code>color-dodge</code> رنگ زیرین را به معکوس رنگ بالا تقسیم می‌کند.</li>
<li><code>color-burn</code> معکوس رنگ زیرین را به رنگ بالا تقسیم می‌کند.</li>
<li><code>hard-light</code> شبیه به overlay اما جای ترسیمات جدید و قبلی عوض می‌شود.</li>
<li><code>soft-light</code> نسخه‌ی ملایم‌تری از hard-light که رنگ‌ها در آن بیشتر ترکیب می‌شوند.</li>
<li><code>difference</code> تفاوت لایه‌ی زیرین را از لایه‌ی بالا استفاده می‌کند. اگر مقدار منفی شود آن را قرینه می‌کند.</li>
<li><code>exclusion</code> شبیه به difference اما با contrast کمتر</li>
<li><code>hue</code> نتیجه‌ی این مقدار یک رنگ HSL است که saturation و luma آن از رنگ قبلی، و hue آن از رنگ جدید است.</li>
<li><code>saturation</code> شبیه به hue اما با این مقدار، saturation از ترسیمات جدید استفاده می‌شود.</li>
<li><code>luminosity</code> شبیه به hue و saturation اما با این مقدار، luma یا رنگ از ترسیمات جدید استفاده می‌شود.</li>
<li><code>color</code> برعکس luminosity از رنگ ترسیمات قبلی استفاده شده ولی دیگر مقادیر از ترسیمات جدید استفاده می‌شوند.</li>
<li><code>darker</code> این مقدار در نسخه‌های قدیمی مرورگر‌ها وجود داشت اما به دلایلی دیگر پشتیبانی نمی‌شود.</li>
</ul>
<p>همانطور که می‌بینید، تعداد این مقدایر زیاد بوده و رفتار هرکدام نیز با دیگر مقادیر تفاوت زیادی دارد. از بین این 26 مقدار، چهار مقدار مربوط به source، چهار مقدار مربوط به destination، و دو مقدار <code>xor</code> و <code>copy</code> هستند که به ساختار کلی ترسیمات اثر می‌گذارند. دیگر مقادیر همگی در بازه‌ی رنگ‌ها هستند. این موارد می‌توانند در ایجاد افکت‌های تصویری بسیار کاربردی باشند اما به اندازه‌ی مقدار‌های قبلی در همه‌جا کاربرد ندارند.</p>
<p>ابتدا به ده مقدار اول می‌پردازیم، زیرا برای دیدن رفتار این ده مقدار نیاز به شکل‌های پیچیده نیست. در شکل زیر رفتار این ده مقدار نشان داده شده. توجه کنید که این ده مقدار روی ساختار ترسیمات اثر می‌گذارند و یادگیری آن‌ها نسبت به دیگر مقادیر مهم‌تر است. در شکل زیر مستطیل حکم ترسیمات قبلی، و دایره حکم ترسیمات جدید را دارد:</p>
<p><img decoding="async" src="https://css-tricks.ir/wp-content/uploads/2021/09/canvas-main-compositions.png" alt="canvas main compositions" /></p>
<p>حال به مقدار‌های مربوط به رنگ می‌پردازیم. برای درک بهتر رفتار آن‌ها باید از ترسیماتی استفاده کنیم که دارای طیف رنگ و شفافیت هستند. کد زیر یک تصویر مناسب برای این کار ایجاد می‌کند. در کد زیر به جای مقدار <code>source-over</code> مقدار‌های متفاوت قرار داده و نتیجه را ببینید. همچنین می‌توانید از تصویر پایین کمک بگیرید:</p>
<pre><code class="language-javascript">
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();
</code></pre>
<p><img decoding="async" src="https://css-tricks.ir/wp-content/uploads/2021/09/canvas-color-compositions.png" alt="canvas color compositions" /></p>
<p>شاید در نگاه اول بسیار پیچیده به نظر برسند، اما وقتی توضیحات آن‌ها را بخوانید بهتر متوجه کارکرد آن‌ها می‌شوید. حال که بیشتر و بهتر با این مقدار‌ها آشنا شدیم، زمان آن است که به برخی از کاربرد‌های آن‌ها اشاره کنیم. در این بخش مروری کوتاه به برخی موارد خواهیم داشت اما در پروژه‌های آینده با کاربرد‌های بیشتری آشنا خواهید شد.</p>
<h3>پاک کردن لایه‌ی ترسیمات به شکل دلخواه</h3>
<p>در موارد بسیاری پیش می‌آید که بخواهید مساحتی به شکل یک دایره یا چند‌ضلعی را از لایه‌ی ترسیمات پاک کنید. در این موارد، می‌توانید از مقدار <code>destination-out</code> استفاده کنید تا شکل فعلی به جای رسم شدن، لایه‌ی ترسیمات را به شکل خودش پاک کند. به کد زیر توجه کنید. در کد زیر یک مستطیل سبز وارد لایه‌ی ترسیمات می‌شود، سپس مساحتی به شکل یک دایره لایه‌ی ترسیمات را به کمک این مقدار پاک کرده و بخشی از مستطیل نیز پاک می‌شود:</p>
<pre><code class="language-javascript">
ctx.fillStyle = "#3D3";
ctx.fillRect(50, 50, 300, 200);

ctx.globalCompositeOperation = "destination-out";

ctx.arc(160, 200, 130, 0, Math.PI * 2);
ctx.fill();
</code></pre>
<h3>محدود کردن لایه‌ی ترسیمات بدون نیاز به شکل فعلی</h3>
<p>گاهی پیش می‌آید که می‌خواهید لایه‌ی ترسیمات را محدود کنید اما بنا به دلایلی نمی‌توانید شکل فعلی را به درستی تعیین کرده یا آن را تغییر دهید. در این موقعیت می‌توانید از مقدار <code>source-atop</code> استفاده کنید. با این مقدار می‌توانید بدون نیاز به تعیین شکل فعلی و استفاده از متد <code>clip</code>، ترسیمات بعدی را به محدوده‌ی ترسیمات فعلی درون لایه‌ی ترسیمات محدود کنید. این ویژگی را برای کد بالا امتحان کرده و نتیجه را ببینید. بخش‌هایی از دایره که خارج از محدوده‌ی ترسیمات قبلی (همان مستطیل) باشند، رسم نمی‌شوند.</p>
<h3>پاک کردن لایه‌ی ترسیمات بدون دردسر</h3>
<p>در canvas روش‌های زیادی برای پاک کردن لایه‌ی ترسیمات وجود دارد، منتها همگی حداقل یک مشکل همراه خود دارند. ساده‌ترین روش این است که از متد <code>clearRect</code> استفاده کنیم. اما این متد از متد‌های transform تاثیر می‌پذیرد. با وجود اینکه می‌توان متد‌های transform را دور زد، اما روش بسیار ساده‌ای برای پاک کردن لایه‌ی ترسیمات وجود دارد و آن استفاده از مقدار <code>copy</code> است! به کد زیر توجه کنید:</p>
<pre><code class="language-javascript">
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;
}
</code></pre>
<p>کد بالا یک تابع تعریف می‌کند که زمینه‌ی موردنظر را به عنوان ورودی دریافت می‌کند. سپس مقدار فعلی ویژگی <code>globalCompositeOperation</code> و <code>fillStyle</code> را ذخیره می‌کند؛ سپس آن را به <code>copy</code> تغییر داده و رنگ را نیز کاملا شفاف می‌کند. حال یک مربع به طول و عرض 1 رسم می‌کند. سپس مقدار قبلی ویژگی‌ها را پس می‌دهد. این روش یکی از ساده‌ترین و سریع‌ترین روش‌ها برای پاک کردن ترسیمات canvas است. در آموزش‌های آینده توضیحات بیشتری در این باره آورده شده است.</p>
<h3>ایجاد توهم سه‌بعدی</h3>
<p>با اینکه در canvas نمی‌توان ترسیمات سه‌بعدی انجام داد، اما اگر مقدار <code>destination-over</code> را با سایه و رنگ درست ترکیب کنید، می‌توانید یک توهم سه‌بعدی زیبا ایجاد کنید. از این ویژگی در ترسیمات پیشرفته در آینده استفاده خواهد شد.</p>
<h3>ایجاد جلوه‌های تصویری</h3>
<p>هرچند روش‌های دیگری نیز برای ایجاد جلوه روی تصاویر وجود دارد، اما سرعت پایینی دارند. با توجه به گستردگی مقادیر مربوط به رنگ در این ویژگی، اگر بتوانید ترکیب درستی از ترسیمات را روی تصاویر اعمال کنید، می‌توانید جلوه‌های بسیار سنگین را با کمترین خط کد و با بیشترین سرعت اجرا کنید! در بخش‌های آینده بیشتر به این مورد خواهیم پرداخت. فعلا به این مورد کوچک توجه کنید. متغیر <code>img</code> یک تصویر در صفحه است که می‌توانید آن را به دلخواه تعیین کنید:</p>
<pre><code class="language-javascript">
let img = document.getElementById("an_image");

cvs.width = 1500;
cvs.height = 500;

onload = () =&gt; {
    img = (() =&gt; {
        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);
}
</code></pre>
<p><img decoding="async" src="https://css-tricks.ir/wp-content/uploads/2021/09/canvas-filter-composition.png" alt="canvas image effect compositions" /></p>
<h2>نکات پایانی</h2>
<p>جدا از مواردی که بررسی شد، دو نکته‌ی مهم درباره‌ی این ویژگی وجود دارد که باید همیشه به آن‌ها توجه داشته باشید.</p>
<h3>نوع ترسیمات روی نتیجه اثرگذار هستند</h3>
<p>این مورد با اینکه تا حدودی بدیهی است، اما بسیاری آن را فراموش می‌کنند. گفتیم ویژگی <code>globalCompositeOperation</code> نوع تعامل ترسیمات جدید را با ترسیمات پیشین تعیین می‌کند. این یعنی اگر شکل با متد <code>stroke</code> رسم شود، نتیجه‌ی متفاوتی با متد <code>fill</code> خواهد داشت. حتی الگوی ترسیمات (ورودی متد <code>fill</code>) نیز می‌توانند نتایج متفاوتی داشته باشند! برای درک بهتر موضوع، در کد‌های بالا، به جای متد <code>fill</code> از متد <code>stroke</code> استفاده کنید تا بهتر متوجه شوید.</p>
<h3>وجود شفافیت در ترسیمات مهم است</h3>
<p>در اینجا متوجه می‌شوید که کاربرد ویژگی <code>globalAlpha</code> آنقدرها هم کم نیست! شفافیتی که در ترسیمات وجود دارد، روی رفتار <code>globalCompositeOperation</code> اثرگذار است. در واقع این ویژگی، بسته به میزان شفافیت ترسیمات روی آن‌ها اثر می‌گذارد. برای نمونه اگر ترسیمات دارای 95% شفافیت باشند، این ویژگی فقط روی 5% غیرشفاف تاثیر می‌گذارد. برای درک بهتر به کد زیر توجه کنید:</p>
<pre><code class="language-javascript">
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", () =&gt; {
    ctx.globalCompositeOperation = "destination-out";
    ctx.fill();
});
</code></pre>
<p>در کد بالا که مشابه کد قبل برای پاک کردن ترسیمات است، شکل اولیه رسم شده، سپس یک دایره برای پاک کردن لایه‌ی ترسیمات وارد شکل فعلی می‌شود، نکته‌ی مهم این است که ویژگی <code>fillStyle</code> دارای شفافیت 90% است، این یعنی ویژگی <code>globalCompositeOperation</code> بسته به این شفافیت عمل می‌کند. با هر بار کلیک روی عنصر <code>cvs</code>، متد <code>fill</code> یک بار اجرا می‌شود.</p>
<p>احتمالا انتظار دارید مانند کد قبل، با یک بار کلیک تمام ترسیمات که زیر دایره قرار می‌گیرند پاک شوند، اما بعد از امتحان کردن متوجه این ویژگی خواهید شد! همانطور که می‌بینید، با هر بار کلیک فقط 10% شفافیت ترسیمات کم می‌شود، علت این است که ترسیمات جدید 90% شفافیت داشتند و این ویژگی روی بخش‌های شفاف ترسیمات کار نمی‌کند. (درست شبیه سایه)</p>
<p>ترکیب شفافیت با این ویژگی (چه به واسطه‌ی فرمت رنگ <code>rgba</code>، <code>hsla</code>، یا ویژگی <code>globalAlpha</code>)، کاربرد‌های این ویژگی را باز هم بیشتر می‌کند. استفاده از این ترکیب در انیمیشن‌ها می‌تواند ترسیمات فوق‌العاده‌ای ایجاد کند که بازسازی آن‌ها بدون این ترکیب غیرممکن است!</p>
<p>جدا از تمام مواردی که گفته شد، لازم است اشاره‌ای نیز به دو متد <code>setAlpha</code> و <code>setCompositeOperation</code> داشته باشیم. این دو متد در مرورگر‌های رده webkit پشتیبانی می‌شدند اما سال‌ها پیش منقضی شده‌اند.</p>
<h2>نتیجه‌گیری</h2>
<p>همانطور که تاکنون متوجه شده‌اید، این دو ویژگی (برخلاف ظاهرشان) بسیار کاربردی هستند و با کوچکترین تغییر، بیشترین تاثیر را بر ترسیمات می‌گذارند. پیشنهاد می‌شود این بخش را به خوبی مطالعه کرده و یاد بگیرید، زیرا این موارد جزء مهم‌ترین ویژگی‌های canvas هستند. کاربرد‌هایی که در این بخش بررسی شد، فقط چند مورد جزئی از کاربرد‌های فراوان این ویژگی (یا ترکیب این دو ویژگی) هستند. در بخش بعدی به تبدیلات (transform) خواهیم پرداخت.</p>
<p>نوشته <a rel="nofollow" href="https://css-tricks.ir/tutorial/%d8%aa%d8%b9%d8%a7%d9%85%d9%84-%d8%b4%da%a9%d9%84-%d9%81%d8%b9%d9%84%db%8c-%d9%88-%d9%84%d8%a7%db%8c%d9%87%e2%80%8c%db%8c-%d8%aa%d8%b1%d8%b3%db%8c%d9%85%d8%a7%d8%aa-%d8%af%d8%b1-canvas/">تعامل شکل فعلی و لایه‌ی ترسیمات در canvas</a> اولین بار در <a rel="nofollow" href="https://css-tricks.ir">آموزش طراحی وب</a> پدیدار شد.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.ir/tutorial/%d8%aa%d8%b9%d8%a7%d9%85%d9%84-%d8%b4%da%a9%d9%84-%d9%81%d8%b9%d9%84%db%8c-%d9%88-%d9%84%d8%a7%db%8c%d9%87%e2%80%8c%db%8c-%d8%aa%d8%b1%d8%b3%db%8c%d9%85%d8%a7%d8%aa-%d8%af%d8%b1-canvas/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>متن در canvas</title>
		<link>https://css-tricks.ir/tutorial/%d9%85%d8%aa%d9%86-%d8%af%d8%b1-canvas/</link>
					<comments>https://css-tricks.ir/tutorial/%d9%85%d8%aa%d9%86-%d8%af%d8%b1-canvas/#respond</comments>
		
		<dc:creator><![CDATA[حسین رفیعی]]></dc:creator>
		<pubDate>Sat, 02 Oct 2021 04:10:46 +0000</pubDate>
				<category><![CDATA[آموزش]]></category>
		<category><![CDATA[canvas]]></category>
		<category><![CDATA[text]]></category>
		<category><![CDATA[متن]]></category>
		<guid isPermaLink="false">https://css-tricks.ir/?p=9393</guid>

					<description><![CDATA[<p>در این بخش به متد‌ها و ویژگی‌های مربوط به متن در canvas پرداخته و یک نمونه انیمیشن از آن نیز بررسی می‌کنیم.</p>
<p>نوشته <a rel="nofollow" href="https://css-tricks.ir/tutorial/%d9%85%d8%aa%d9%86-%d8%af%d8%b1-canvas/">متن در canvas</a> اولین بار در <a rel="nofollow" href="https://css-tricks.ir">آموزش طراحی وب</a> پدیدار شد.</p>
]]></description>
										<content:encoded><![CDATA[<p>هرچند امکانات متن در SVG بیشتر هستند، اما باز هم canvas در نوشتن متن حرفی برای گفتن دارد! گاهی لازم است درون canvas چیزی نوشته شود. برای این موقعیت‌ها متد‌هایی وجود دارند که می‌توانند نیاز ما را برطرف کنند. در این آموزش به نوشتن متن در canvas می‌پردازیم.</p>
<h2>متد fillText</h2>
<p>با استفاده از این متد می‌توان متن مورد‌نظر را به صورت <code>fill</code> رسم کرد. این متد سه ورودی اصلی می‌پذیرد که به ترتیب متن مورد‌نظر، و مختصات رسم هستند. ورودی چهارم که اختیاری است طول متن را مشخص می‌کند. اگر طول متن بیشتر از این مقدار باشد، متن فشرده (scale) شده تا در این طول جا شود:</p>
<pre><code class="language-javascript">
ctx.fillText("text", X, Y, max_width);
</code></pre>
<h2>متد strokeText</h2>
<p>این متد نیز درست شبیه به متد قبل سه ورودی اصلی و یک ورودی اختیاری دارد. این متد متن مورد‌نظر را به صورت <code>stroke</code> رسم می‌کند و طبعا هر چیزی که به ترسیمات <code>stroke</code> اثر بگذارد، به متنی که با این متد رسم شود نیز تاثیر می‌گذارد:</p>
<pre><code class="language-javascript">
ctx.strokeText("text", X, Y, max_width);
</code></pre>
<p>متن‌هایی که رسم می‌شوند می‌توانند حاشیه، خط‌چین، سایه، طیف رنگ، الگو، و خلاصه تمام ویژگی‌های دیگر ترسیمات در canvas را داشته باشند. مشکل اصلی این است که متن هیچگاه وارد شکل فعلی نمی‌شود و همچنین تعیین مختصات متن در کنار دیگر ویژگی‌های مربوط به متن کمی دردسر‌ساز است.</p>
<h2>ویژگی textAlign</h2>
<p>این ویژگی رفتاری مشابه <a href="https://css-tricks.ir/reference/text-align/">ویژگی text-align</a> در CSS دارد و از تکرار مطالب آن خودداری می‌کنیم. این ویژگی مقدار‌های زیر را می‌پذیرد. مختصات X ورودی محل این ویژگی را تعیین کرده و متن نسبت به این خط رسم می‌شود:</p>
<pre><code class="language-javascript">
ctx.textAlign = "start" || "end" || "left" || "right" || "middle";
</code></pre>
<p>مختصات متن نسبت به این ویژگی رسم می‌شود، یعنی مثلا اگر مقدار آن <code>left</code> باشد، متن در آن مختصات X آغاز می‌شود، ولی اگر مقدار آن <code>right</code> باشد، متن در آن مختصات X پایان می‌یابد. مقدار <code>center</code> نیز باعث می‌شود مختصات وسط متن در آن مختصات X قرار بگیرد. برای درک بهتر موضوع کد زیر را اجرا کنید:</p>
<pre><code class="language-javascript">
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);
</code></pre>
<h2>ویژگی font</h2>
<p>این ویژگی نیز دقیقا مانند <a href="https://css-tricks.ir/reference/font/">ویژگی font</a> در CSS رفتار می‌کند، با این تفاوت که نیازی به تعیین <code>line-height</code> ندارد. در کد زیر مقدار کامل این ویژگی نوشته شده است اما همانطور که گفته شد می‌توانید از نوشتن <code>line-height</code> خودداری کنید. در برخی مرورگر‌ها این ویژگی خود‌به‌خود حذف می‌شود:</p>
<pre><code class="language-javascript">
ctx.font = "[font-style] [font-variant] [font-weight] [font-size] / [line-height] [font-family]";
</code></pre>
<h2>متد measureText</h2>
<p>این متد یک ورودی از نوع متن می‌پذیرد و ویژگی‌های مختلف آن را اندازه‌گیری کرده و در یک شئ از نوع <code>TextMetrics</code> برمی‌گرداند. این متد می‌تواند ویژگی‌های زیادی از متن را اندازه‌گیری کند، اما فقط اندازه‌گیری طول متن به‌خوبی پشتیبانی می‌شود! این متد از ویژگی <code>font</code> تاثیر می‌پذیرد. به نمونه کد زیر توجه کنید:</p>
<pre><code class="language-javascript">
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 } */
</code></pre>
<h2>ویژگی textBaseline</h2>
<p>این ویژگی نیز خط مبنای متن را مشخص می‌کند که متن نسبت به آن رسم می‌شود. این ویژگی نیز شبیه به <a href="https://css-tricks.ir/reference/vertical-align/">ویژگی vertical-align</a> در CSS است اما برخی موارد آن نیاز به توضیح دارند. این ویژگی مقادیر زیر را می‌پذیرد:</p>
<pre><code class="language-javascript">
ctx.textBaseline = "top" || "hanging" || "middle" ||
                   "alphabetical" || "ideographic" || "bottom";
</code></pre>
<p>محل این خطوط در تصویر زیر مشخص شده اما رفتار هر خط شاید واضح نباشد. مختصات این خط نسبت به متن مشخص نمی‌شود، بلکه مختصات متن نسبت به این خط مشخص می‌شود! و مختصات این خط نیز توسط ورودی متد رسم متن مشخص می‌شود. برای نمونه اگر مختصات ورودی <code>(500,300)</code> باشد، مختصات Y این خط نیز 300 خواهد بود، سپس با توجه به ویژگی <code>textBaseline</code>، موقعیت متن نسبت به این خط مشخص شده و رسم می‌شود. مثلا اگر این ویژگی برابر <code>top</code> باشد، متن زیر آن نوشته می‌شود! (به تصویر دقت کنید تا بهتر متوجه شوید)</p>
<p><img decoding="async" src="https://css-tricks.ir/wp-content/uploads/2021/09/canvas-text-baseline.png" alt="canvas text baseline" /></p>
<h2>بررسی یک نمونه</h2>
<p>با توجه به موارد گفته شده، در این بخش یک نمونه بررسی می‌شود. خواهشمندیم کد‌های زیر را به‌خوبی بررسی کرده و یاد بگیرید زیرا این موارد جزو ساده‌ترین کد‌ها در ایجاد انیمیشن یا ترسیمات پیچیده هستند و باید از هم‌اکنون به یادگیری آن‌ها بپردازید تا بعد‌ها به مشکل برنخورید. برای هر بخش از کد‌ها توضیحات موردنیاز نیز آورده شده است:</p>
<pre><code class="language-markup">
&lt;canvas id="cvs" style="border: 0.1em solid #111;"&gt;&lt;/canvas&gt;
</code></pre>
<pre><code class="language-javascript">

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) =&gt; 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 &lt; l; i += 2) {
    ctx.lineTo(vertices[i], vertices[i + 1]);
}

/* (11) */
setInterval(draw_text, 16);
</code></pre>
<p><img decoding="async" src="https://css-tricks.ir/wp-content/uploads/2021/09/canvas-animated-line-dash.png" alt="canvas dash-line animation" /></p>
<h3>بخش 1 تعریف ویژگی‌های اولیه</h3>
<p>در این بخش ویژگی‌های اولیه، از جمله <code>font</code>، <code>lineJoin</code>، <code>lineCap</code>، و خط‌چین را تعیین می‌کنیم. از آنجایی که می‌خواهیم متن در وسط عنصر <code>cvs</code> باشد، برای راحتی کار ویژگی‌های <code>textAlign</code> و <code>textBaseline</code> را به ترتیب به <code>center</code> و <code>middle</code> تعیین می‌کنیم.</p>
<h3>بخش 2 تعریف طیف رنگ</h3>
<p>در این بخش یک طیف رنگ شعاعی تعریف می‌کنیم که مرکز هر دو دایره‌ی آن وسط <code>cvs</code> بوده و شعاع دایره‌ی بزرگ‌تر نیز 350 است. رنگ‌های این طیف به گونه‌ای هستند که به آرامی تغییر نمی‌کنند بلکه دارای لبه‌های مشخص هستند. روش ساخت این نوع طیف رنگ در <a href="https://css-tricks.ir/reference/linear-gradient/">این پست</a> به‌خوبی توضیح داده شده است.</p>
<h3>بخش 3 تعریف مختصات نقاط شکل</h3>
<p>در این بخش یک آرایه به نام <code>vertices</code> تعریف می‌شود. این آرایه شامل نقاطی است که می‌خواهیم به آن‌ها خط رسم کنیم. می‌توانستیم این کار را به صورت دستی انجام داده و صد‌ها خط کد تکراری بنویسیم اما با این ترفند به سادگی می‌توانیم هر شکل بزرگ و پیچیده‌ای را در چند خط ساده رسم کنیم.</p>
<h3>بخش 4 مجموع اعضای آرایه‌ی خط‌چین</h3>
<p>در این بخش با کمک متد <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce">reduce</a> که یک متد ویژه‌ی آرایه است، مجموع اعداد درون آرایه‌ی خط‌چین را به دست آورده و در متغیر <code>offset_size</code> ذخیره می‌کنیم. درضمن یک متغیر به نام <code>dash_offset</code> تعریف می‌کنیم. قرار است برای خط‌چین شکل و متن یک انیمیشن ایجاد کنیم و برای این کار به این متغیر‌ها نیاز داریم.</p>
<h3>بخش 5 تابع انیمیشن</h3>
<p>در این بخش تابع <code>draw_text</code> را تعریف می‌کنیم که تابع اصلی ما در اجرای انیمیشن است. خود تابع نیاز به توضیحات خاصی ندارد و به توضیح کد درون آن می‌پردازیم.</p>
<h3>بخش 6 آماده‌سازی اولیه</h3>
<p>در این بخش لایه‌ی ترسیمات کاملا پاک می‌شود تا ترسیمات بعدی روی آن رسم شوند. همچنین، ویژگی <code>lineDashOffset</code> برابر مقدار متغیر <code>dash_offset</code> قرار می‌گیرد. این ویژگی تا پایان اجرای یک فریم از انیمیشن بدون تغییر باقی می‌ماند.</p>
<h3>بخش 7 رسم لایه‌ی بیرونی</h3>
<p>در این بخش رنگ حاشیه به سیاه و اندازه‌ی حاشیه به 13 تعیین می‌شود و متن و شکل رسم می‌شوند. توجه کنید که منظور از شکل، شکلی است که از رسم نقاط درون آرایه‌ی <code>vertices</code> ایجاد شده. این شکل یک بار تعریف شده ولی تا وقتی که بخواهیم درون شکل فعلی باقی می‌ماند.</p>
<h3>بخش 8 رسم لایه‌ی اصلی</h3>
<p>در این بخش رنگ حاشیه به طیف رنگ <code>gradient</code> که پیش‌تر تعریف کردیم و اندازه‌ی حاشیه به 8 تعیین می‌شود. این بار شکل و متن، با حاشیه‌ی کوچکتر و رنگ متفاوت روی حاشیه‌ای که در بخش قبل رسم شد، رسم می‌شوند و این توهم را به وجود می‌آورند که انگار حاشیه‌ی شکل، خود دارای حاشیه شده!</p>
<h3>بخش 9 ایجاد انیمیشن</h3>
<p>در این بخش یک واحد به متغیر <code>dash_offset</code> اضافه می‌شود؛ البته به واسطه‌ی عملگر <code>%</code> و متغیر <code>offset_width</code>، اطمینان حاصل می‌شود که مقدار آن از مجموع اعضای آرایه‌ی خط‌چین بیشتر نشود. این مورد ضروری نیست اما بنا به خاصیت پله‌ای خط‌چین، می‌توانیم از بزرگ شدن بی‌مورد آن جلوگیری کنیم و همان نتیجه را بگیریم. حال که یک واحد به این متغیر اضافه شده، در فریم بعدی، مقدار آن به <code>lineDashOffset</code> رسیده و باعث ایجاد انیمیشن روی خط‌چین می‌شود.</p>
<h3>بخش 10 رسم خودکار خطوط</h3>
<p>در این بخش از آرایه‌ی <code>vertices</code> استفاده می‌کنیم. در این بخش یک حلقه داریم که اعداد درون آرایه را به صورت جفتی انتخاب کرده و در متد <code>lineTo</code> قرار می‌دهد. به این ترتیب شکل موردنظر رسم می‌شود. منظور از انتخاب به صورت جفتی این است که گام حلقه 2 است و در هر بار اجرای حلقه می‌توانیم به عضو <code>i</code> و <code>i+1</code> ام دسترسی داشته باشیم.</p>
<h3>بخش 11 اجرای انیمیشن</h3>
<p>در این بخش به کمک تابع <code>setInterval</code> یک انیمیشن ایجاد می‌شود که هر 16 میلی‌ثانیه یک بار اجرا می‌شود. هر 16 میلی‌ثانیه یک بار، تابع <code>draw_text</code> اجرا می‌شود؛ یعنی حدود 60 بار در یک ثانیه. با هر بار اجرای این تابع، یک واحد به <code>dash_offset</code> اضافه می‌شود و به این ترتیب حاشیه‌ی شکل دارای انیمیشن می‌شود.</p>
<p class="oi-content oi-content--warning">تابع setInterval برای ایجاد انیمیشن مناسب نیست و در اینجا فقط از جهت آشنایی استفاده شده. در بخش «انیمیشن در canvas» مفصل به این موضوع پرداخته و توابع مناسب را بررسی می‌کنیم.</p>
<h3>توضیحات تکمیلی</h3>
<p>از جمله مواردی که ممکن است در کد بالا شما را گیج کرده باشند، می‌توان به عملگر <code>%</code> اشاره کرد. این عملگر باقی‌مانده‌ی تقسیم عدد سمت چپ را نسبت به عدد راست برمی‌گرداند. از خواص تقسیم این است که باقی‌مانده‌ی یک تقسیم هیچ‌گاه نمی‌تواند بزرگ‌تر از مقسوم‌علیه آن تقسیم (یعنی عدد سمت راست) بشود. ما از این ویژگی استفاده کردیم و باقی‌مانده‌ی تقسیم <code>dash_offset</code> بر <code>offset_width</code> را در خود متغیر <code>dash_offset</code> ذخیره کردیم. یعنی <code>offset_width</code> مقسوم‌علیه یا مخرج کسر ماست بنابراین عدد نهایی که باقی‌مانده‌ی تقسیم است، هیچ‌گاه از آن بزرگ‌تر نمی‌شود. این ویژگی تقسیم باعث شده نه‌تنها در اینجا، بلکه در پروژه‌های فراوان دیگری نیز از این عملگر برای هدف مشابه استفاده شود. ویژگی این عملگر، به‌ویژه در کار با آرایه‌ها، بسیار کاربردی است.</p>
<h2>مشکلات کار با متن در canvas</h2>
<p>متاسفانه canvas و SVG امکانات چندان مناسبی برای کار با متن ندارند. در واقع هدف اصلی این فناوری‌ها کار با متن نبوده و نیست. علاوه بر اینکه نوشتن متن به پردازش زیادی نیاز دارد، حتی متن مورد‌نظر وارد شکل فعلی نمی‌شود و این موضوع باعث شده که انجام بسیاری از کار‌ها یا خیلی سخت شده و یا غیرممکن باشد!</p>
<p>با اینکه می‌توان کار‌های جالبی با متن انجام داد، اما در موارد کاربردی‌تر مانند نمایش یک بند نوشته کار بسیار سختی خواهد بود و بهتر است به دنبال روش‌های جایگزین باشیم. در بخش «ترکیب HTML و canvas» به این موضوع خواهیم پرداخت.</p>
<h2>نتیجه‌گیری</h2>
<p>در این بخش سعی کردیم با امکانات مربوط به متن آشنا شده و یک نمونه‌ی نسبتا بزرگ نیز بررسی کردیم. سعی کنید با مواردی که در این آموزش یاد گرفتید یک طرح ایجاد کنید تا درک بهتری از روند طراحی و متد‌های canvas به دست آورید زیرا آموزش‌های آینده سخت‌تر و پیچیده‌تر خواهند بود. در آموزش آینده به «تعامل شکل فعلی و لایه‌ی ترسیمات» می‌پردازیم.</p>
<p>نوشته <a rel="nofollow" href="https://css-tricks.ir/tutorial/%d9%85%d8%aa%d9%86-%d8%af%d8%b1-canvas/">متن در canvas</a> اولین بار در <a rel="nofollow" href="https://css-tricks.ir">آموزش طراحی وب</a> پدیدار شد.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.ir/tutorial/%d9%85%d8%aa%d9%86-%d8%af%d8%b1-canvas/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>آموزش Canvas</title>
		<link>https://css-tricks.ir/reading-list/canvas/</link>
					<comments>https://css-tricks.ir/reading-list/canvas/#respond</comments>
		
		<dc:creator><![CDATA[حسین رفیعی]]></dc:creator>
		<pubDate>Wed, 29 Sep 2021 15:45:31 +0000</pubDate>
				<category><![CDATA[لیست آموزشی]]></category>
		<category><![CDATA[canvas]]></category>
		<category><![CDATA[آموزش canvas]]></category>
		<category><![CDATA[فصل آموزشی]]></category>
		<category><![CDATA[مرجع canvas]]></category>
		<guid isPermaLink="false">https://css-tricks.ir/?p=9465</guid>

					<description><![CDATA[<p>در این فصل آموزشی به صورت قدم به قدم و بطور کامل با مفاهیم Canvas آشنا خواهیم شد. مطالب از ابتدایی تا پیشرفته خواهند بود.</p>
<p>نوشته <a rel="nofollow" href="https://css-tricks.ir/reading-list/canvas/">آموزش Canvas</a> اولین بار در <a rel="nofollow" href="https://css-tricks.ir">آموزش طراحی وب</a> پدیدار شد.</p>
]]></description>
										<content:encoded><![CDATA[<p>مطالب زیر بصورت گام به گام مفاهیم مربوط به Canvas را آموزش می‌دهند:</p>
<a class="reading-part" href="//css-tricks.ir/tutorial/عنصر-canvas-و-کاربردهای-آن/">عنصر canvas و کاربردهای آن</a>
<a class="reading-part" href="//css-tricks.ir/tutorial/آغاز-کار-با-canvas-و-متد‌های-اولیه/">آغاز کار با canvas و متد‌های اولیه</a>
<a class="reading-part" href="//css-tricks.ir/tutorial/شکل‌های-پایه-در-canvas/">شکل‌های پایه در canvas</a>
<a class="reading-part" href="//css-tricks.ir/tutorial/شکل-فعلی،-لایه‌ی-ترسیمات،-و-تنظیمات-canvas/">شکل فعلی، لایه‌ی ترسیمات، و تنظیمات canvas</a>
<a class="reading-part" href="//css-tricks.ir/tutorial/رسم-تصویر-در-canvas/">رسم تصویر در canvas</a>
<a class="reading-part" href="//css-tricks.ir/tutorial/خط‌چین،-الگو،-طیف-رنگ،-و-سایه-در-canvas/">خط‌چین، الگو، طیف رنگ، و سایه در canvas</a>
<a class="reading-part" href="//css-tricks.ir/tutorial/متن-در-canvas/">متن در canvas</a>
<a class="reading-part" href="//css-tricks.ir/tutorial/تعامل-شکل-فعلی-و-لایه‌ی-ترسیمات-در-canvas/">تعامل شکل فعلی و لایه‌ی ترسیمات در canvas</a>
<a class="reading-part" href="//css-tricks.ir/tutorial/تبدیلات-transform-در-canvas/">تبدیلات transform در canvas</a>
<a class="reading-part" href="//css-tricks.ir/tutorial/متد‌های-save-و-restore-در-canvas/">متد‌های save و restore در canvas</a>
<a class="reading-part" href="//css-tricks.ir/tutorial/انیمیشن-در-canvas/">انیمیشن در canvas</a>
<a class="reading-part" href="//css-tricks.ir/tutorial/کلاس-imagedata-در-canvas/">کلاس ImageDate در canvas</a>
<a class="reading-part" href="//css-tricks.ir/tutorial/کلاس-path2d-قدرت-جدید-canvas/">کلاس Path2D قدرت جدید canvas</a>
<a class="reading-part" href="//css-tricks.ir/tutorial/تعیین-اندازه-canvas-و-تعامل-آن-با-css/">تعیین اندازه canvas و تعامل آن با css</a>
<p>مطالب این فصل ادامه دارند و به مرور به آخر این لیست اضافه خواهند شد.</p>
<p>نوشته <a rel="nofollow" href="https://css-tricks.ir/reading-list/canvas/">آموزش Canvas</a> اولین بار در <a rel="nofollow" href="https://css-tricks.ir">آموزش طراحی وب</a> پدیدار شد.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.ir/reading-list/canvas/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>خط‌چین، الگو، طیف رنگ، و سایه در canvas</title>
		<link>https://css-tricks.ir/tutorial/%d8%ae%d8%b7%e2%80%8c%da%86%db%8c%d9%86%d8%8c-%d8%a7%d9%84%da%af%d9%88%d8%8c-%d8%b7%db%8c%d9%81-%d8%b1%d9%86%da%af%d8%8c-%d9%88-%d8%b3%d8%a7%db%8c%d9%87-%d8%af%d8%b1-canvas/</link>
					<comments>https://css-tricks.ir/tutorial/%d8%ae%d8%b7%e2%80%8c%da%86%db%8c%d9%86%d8%8c-%d8%a7%d9%84%da%af%d9%88%d8%8c-%d8%b7%db%8c%d9%81-%d8%b1%d9%86%da%af%d8%8c-%d9%88-%d8%b3%d8%a7%db%8c%d9%87-%d8%af%d8%b1-canvas/#respond</comments>
		
		<dc:creator><![CDATA[حسین رفیعی]]></dc:creator>
		<pubDate>Sun, 26 Sep 2021 04:30:37 +0000</pubDate>
				<category><![CDATA[آموزش]]></category>
		<category><![CDATA[canvas]]></category>
		<category><![CDATA[gradient]]></category>
		<category><![CDATA[line-dash]]></category>
		<category><![CDATA[pattern]]></category>
		<category><![CDATA[shadow]]></category>
		<guid isPermaLink="false">https://css-tricks.ir/?p=9375</guid>

					<description><![CDATA[<p>در این آموزش به بررسی خط‌چین، الگو، طیف‌های رنگ، و سایه می‌پردازیم و ویژگی‌های هرکدام را مورد بررسی قرار می‌دهیم.</p>
<p>نوشته <a rel="nofollow" href="https://css-tricks.ir/tutorial/%d8%ae%d8%b7%e2%80%8c%da%86%db%8c%d9%86%d8%8c-%d8%a7%d9%84%da%af%d9%88%d8%8c-%d8%b7%db%8c%d9%81-%d8%b1%d9%86%da%af%d8%8c-%d9%88-%d8%b3%d8%a7%db%8c%d9%87-%d8%af%d8%b1-canvas/">خط‌چین، الگو، طیف رنگ، و سایه در canvas</a> اولین بار در <a rel="nofollow" href="https://css-tricks.ir">آموزش طراحی وب</a> پدیدار شد.</p>
]]></description>
										<content:encoded><![CDATA[<p>در canvas نیز درست مانند SVG می‌توان طیف رنگ ساخت، حاشیه‌ها را خط‌چین کرد، الگو ساخت، و حتی بدون کمک CSS برای ترسیمات سایه ایجادکرد! در این آموزش به تک‌تک این موارد می‌پردازیم و ویژگی‌ها و موارد ویژه‌ی هرکدام را بررسی می‌کنیم. این آموزش نسبت به دیگر آموزش‌ها کمی طولانی‌تر است اما می‌توانید بدون هیچ مشکلی هرکدام از چهار موضوع را جداگانه بیاموزید.</p>
<h2>خط‌چین (line-dash)</h2>
<p>رسم خط‌چین در ابتدا بخشی از canvas نبود اما امروزه بخشی از استاندارد آن است و پشتیبانی خیلی خوبی دارد. برای رسم خط‌چین دو متد و یک ویژگی وجود دارد که به توضیح هر یک می‌پردازیم. همانطور که مشخص است، این ویژگی زمانی اعمال می‌شود که به شکل حاشیه داده شود.</p>
<h3>متد setLineDash</h3>
<p>برای ایجاد خط‌چین به یک جفت عدد نیاز داریم. طول خط، و فاصله از خط بعدی. اما canvas یک قدم فراتر گذاشته و به جای یک جفت عدد، یک آرایه از اعداد می‌پذیرد که این یعنی می‌توان در canvas خط‌چین‌های پیچیده‌تر ایجاد کرد. طول این آرایه نیز هیچ محدودیتی ندارد. این آرایه باید به عنوان ورودی متد <code>setLineDash</code> قرار بگیرد. این متد فقط همین ورودی را دریافت می‌کند. آرایه‌ی پیش‌فرض برای خط‌چین، یک آرایه‌ی خالی است. در نمونه کد زیر چند نمونه خط‌چین ساده و پیچیده ایجاد می‌شود:</p>
<pre><code class="language-javascript">
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]);
</code></pre>
<h3>متد getLineDash</h3>
<p>این متد آرایه‌ی خط‌چین را برمی‌گرداند و طبعا هیچ ورودی‌ای نمی‌پذیرد. در مواردی لازم است از آرایه‌ی فعلی خط‌چین برای ساختن آرایه‌ی بعدی استفاده شود. در این موارد این متد می‌تواند به ما کمک کند. به نمونه کد زیر دقت کنید. در کد زیر چند نمونه آرایه برای خط‌چین تعیین می‌شود و سپس با استفاده از متد <code>getLineDash</code> دوباره دریافت می‌شوند:</p>
<pre><code class="language-javascript">
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] (!) */
</code></pre>
<p>پرسش مهمی که با بررسی کد بالا پیش می‌آید این است که چرا متد <code>getLineDash</code> برای آرایه‌ی آخر با سه عضو، یک آرایه‌ی شش عضوی برگرداند؟ پاسخ این است که اعداد درون آرایه به صورت جفتی برای خط‌چین استفاده می‌شوند. وقتی یک آرایه با طول فرد به خط‌چین بدهیم، عدد آخر درون آرایه تنها می‌ماند بنابراین یک کپی از این آرایه به انتهای آن اضافه می‌شود تا طول آن زوج شود.</p>
<p>اگر به آرایه‌ی شش عضوی در کد بالا دقت کنید، دقیقا همین اتفاق افتاده است. حال شما یک ترفند عالی برای ساختن خط‌چین‌های باز هم پیچیده‌تر در اختیار دارید! چون در نیمه‌ی دوم آرایه‌ای که ساخته شده، جای طول خط‌چین و فاصله از خطچین بعدی عوض شده؛ و این یعنی باز هم پیچیدگی بیشتر!</p>
<h3>ویژگی lineDashOffset</h3>
<p>این ویژگی فاصله‌ی اولین خط‌چین را از نقطه‌ی آغازین حاشیه تعیین می‌کند. مقدار پیش‌فرض آن صفر است. از این ویژگی می‌توان برای ایجاد انیمیشن روی خط‌چین استفاده کرد. این ویژگی می‌تواند هر مقدار مثبت یا منفی‌ای داشته باشد، اما بنا بر حالت پله‌ای این ویژگی، اگر اندازه‌ی آن از مجموع اعداد آرایه‌ی خط‌چین بیشتر شود، به حالت قبل بازمی‌گردد. برای درک بهتر این موضوع به کد زیر دقت کنید:</p>
<pre><code class="language-javascript">
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 */
</code></pre>
<p>در قسمت اول کد بالا، آرایه‌ی خط‌چین <code>[10,20]</code> است که مجموع اعداد آن 30 می‌شود. این یعنی ویژگی <code>lineDashOffset</code> هر 30 واحد یک بار تکرار می‌شود. این یعنی می‌توانیم به هر تعداد 30 که خواستیم از آن کم کنیم. برای مثال به جای نوشتن 35 برای این ویژگی، می‌توانیم 30 واحد از آن کم کنیم و 5 بنویسیم؛ و همچنان نتیجه‌ی یکسان بگیریم.</p>
<p>قسمت دوم نیز به همین صورت است. مجموع اعداد داخل آرایه 40 می‌شود و این یعنی ویژگی <code>lineDashOffset</code> هر 40 واحد یک بار تکرار می‌شود. یعنی به جای نوشتن 41 می‌توانیم 1 بنویسیم و همان نتیجه را بگیریم. در قسمت سوم این مقدار 15 است و این یعنی به جای نوشتن 15 برای ویژگی <code>lineDashOffset</code> می‌توانیم صفر بنویسیم و نتیجه تغییر نکند. در نمونه کد زیر تمام مقادیری که برای  <code>lineDashOffset</code> نوشته شده‌اند نتیجه‌ی یکسان دارند:</p>
<pre><code class="language-javascript">
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;
</code></pre>
<p>اگر به‌خوبی متوجه «پله‌ای بودن» این ویژگی نشده‌اید، اشکالی ندارد! در بخش انیمیشن حداقل یک مثال در رابطه با این ویژگی بررسی خواهیم کرد و امیدواریم در آنجا این ویژگی را درک کنید!</p>
<p>نکته‌ی مهمی که باید در ذهن داشته باشید این است که تمام ویژگی‌های خط به ویژه <code>lineWidth</code> و <code>lineCap</code> روی خط‌چین اثر می‌گذارند. در نمونه‌هایی که در پایان این بخش بررسی شده‌اند این ویژگی‌ها را امتحان کرده و نتیجه را ببینید.</p>
<p>اگر به یاد داشته باشید، گفتیم که خط‌چین روی رفتار متد <code>isPointInStroke</code> اثر می‌گذارد. اگر برای شکل فعلی یک حاشیه تعریف شده باشد، این متد فقط وقتی مقدار <code>true</code> بازمی‌گرداند که نقطه روی خط‌چین‌ها قرار داشته باشد. برای درک بهتر موضوع کد زیر را بررسی کنید. این کد نسخه‌ی متفاوتی از کد آموزش قبل است که به آن خط‌چین اضافه شده:</p>
<pre><code class="language-javascript">
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);
</code></pre>
<p>در کد زیر چند نمونه‌ی مختلف خط‌چین همراه با نتایج آوره شده‌است. در ردیف دوم خط‌چین‌ها همگی یکسان هستن و در ویژگی <code>lineDashOffset</code> با یکدیگر تفاوت دارند. با اینکه در شکل زیر فقط از مستطیل استفاده شده، اما خط‌چین روی حاشیه‌ی هر نوع شکلی اعمال می‌شود، حتی روی متن! سعی کنید این کد را تغییر دهید تا درک بهتری از موضوع به دست آورید. بهترین راه یادگیری، تمرین عملی است:</p>
<pre><code class="language-javascript">
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);
</code></pre>
<p><img decoding="async" src="https://css-tricks.ir/wp-content/uploads/2021/08/canvas-dash-line.png" alt="canvas line-dash" /></p>
<h2>الگو (pattern)</h2>
<p>شبیه به SVG، از الگو می‌توان به جای رنگ شکل یا رنگ حاشیه استفاده کرد. البته لازم است ابتدا الگوی مورد‌نظر تعریف شود. برای تعریف الگو از متد <code>createPattern</code> استفاده می‌شود. این متد دو ورودی می‌پذیرد که به ترتیب یک عنصر نمایشی برای الگو، و نوع تکرار الگو است. این متد یک شئ از نوع <code>CanvasPattern</code> بازمی‌گرداند که می‌تواند به عنوان مقدار ویژگی <code>fillStyle</code> یا <code>strokeStyle</code> قرار بگیرد و به شکلی که رسم می‌شود اعمال شود:</p>
<pre><code class="language-javascript">
let my_pattern = ctx.createPattern(pattern_img, pattern_repeat);
</code></pre>
<p>ورودی <code>pattern_img</code> که یک عنصر نمایشی است درست شبیه به تصویر ورودی برای متد <code>drawImage</code> است. این ورودی هر چیزی می‌تواند باشد اما باید پیش از فراخوانی این متد بارگیری شده باشد. تمام موارد مربوط به تصویر در اینجا نیز درست هستند و باید رعایت شوند. ورودی <code>pattern_repeat</code> شبیه به ویژگی <a href="https://css-tricks.ir/reference/background-repeat/">background-repeat</a> است و نوع تکرار الگو را مشخص می‌کند. مقدار پیش‌فرض آن &#8220;repeat&#8221; است اما می‌تواند هرکدام از مقادیر &#8220;no-repeat&#8221;، &#8220;repeat-x&#8221; و &#8220;repeat-y&#8221; را نیز داشته باشد.</p>
<p>به نمونه کد زیر دقت کنید. در کد زیر یک <code>canvas</code> دوم (شبیه به آنچه در رسم تصویر انجام دادیم) ایجاد می‌شود و از آن به عنوان الگوی <code>canvas</code> اصلی استفاده کرده و یک مربع رسم می‌کنیم. بیشتر ساختار این کد شبیه به کد قبل است و نیاز به توضیح خاصی ندارد. سعی کنید ترسیمات <code>canvas</code> دوم را تغییر دهید تا الگوی متفاوتی ایجاد کنید:</p>
<pre><code class="language-javascript">
let cvs = document.getElementById("cvs"),
    ctx = cvs.getContext("2d");

cvs.width = cvs.height = 500;

let pattern_image = (() =&gt; {
        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);
</code></pre>
<h2>طیف خط (Gradient)</h2>
<p>در canvas سه نوع طیف رنگ وجود دارد. طیف رنگ خطی، طیف رنگ شعاعی و طیف رنگ مخروطی. روش تعریف و استفاده از این طیف‌ها شبیه به الگو است، با این تفاوت که رنگ‌هایشان باید پیش از استفاده تعریف شوند. درضمن، نمی‌توان یک طیف رنگ یا الگو را تغییر داد و اصلاح کرد.</p>
<p>شبیه به الگو، طیف رنگ ساخته شده توسط یک زمینه، می‌تواند توسط دیگر زمینه‌ها نیز استفاده شود. هنگام ساختن طیف رنگ، یک شئ از نوع <code>CanvasGradient</code> برگردانده می‌شود که یک متد به نام <code>addColorStop</code> دارد. با استفاده از این متد می‌توان به طیف موردنظر رنگ اضافه کرد. طیف رنگ‌های مختلف، متد‌های مختلف با تعداد ورودی مختلف دارند اما همگی این نوع شئ با این متد را برمی‌گردانند.</p>
<p>متد <code>addColorStop</code> دو ورودی می‌پذیرد. ورودی اول یک عدد بین 0 و 1 است که مکان رنگ در طیف را مشخص می‌کند. ورودی دوم نیز رنگ مورد‌نظر است. می‌توان به هر طیف به تعداد دلخواه رنگ اضافه کرد فقط کافیست عدد ورودی بین 0 و 1 باشد. هنگام بررسی هر نوع طیف رنگ یک مثال نیز برای درک بهتر موضوع بررسی می‌شود؛ لطفا کد‌ها را اجرا کنید تا با روش کار هرکدام بهتر آشنا شوید.</p>
<h3>طیف رنگ خطی</h3>
<p>طیف رنگ خطی یا linear-gradient توسط متد <code>createLinearGradient</code> ساخته می‌شود. این متد چهار ورودی می‌پذیرد که نقاط آغاز و پایان طیف هستند. این دو نقطه جهت و اندازه‌ی طیف رنگ را مشخص می‌کنند. به نمونه کد زیر توجه کنید. در کد زیر یک طیف رنگ خطی ساخته می‌شود که آغاز آن در مبدا مختصات و پایان آن در انتهای عنصر است. سپس دو رنگ زرد و سبز به آن اضافه می‌شود. برای درک بهتر نقاط آغاز و پایان طیف، ورودی‌های متد را در کد زیر تغییر دهید و تغییرات در نتیجه را مشاهده کنید:</p>
<pre><code class="language-javascript">
ctx.createLinearGradient(X1, Y1, X2, Y2);
</code></pre>
<pre><code class="language-javascript">
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);
</code></pre>
<h3>طیف رنگ شعاعی</h3>
<p>طیف رنگ شعاعی یا radial-gradient توسط متد <code>createRadialGradient</code> ساخته می‌شود. این متد شش ورودی می‌پذیرد که به ترتیب مختصات مرکز و شعاع دایره‌ی آغاز و پایان هستند. برخلاف CSS، در canvas می‌توان طیف رنگ‌هایی ساخت که مختصات دایره‌هایشان متفاوت است. در کد زیر یک طیف رنگ شعاعی با سه رنگ قرمز، زرد، و سبز ساخته می‌شود. مختصات هر دو دایره در (250,250) قرار دارد. شعاع دایره‌ی کوچکتر 0 و  شعاع دایره‌ی بزرگتر 300 است. می‌توانید مختصات دایره‌ی پایانی را تغییر دهید تا تفاوت آن را با طیف رنگ شعاعی عادی ببینید:</p>
<pre><code class="language-javascript">
ctx.createRadialGradient(X1, Y1, R1, X2, Y2, R2);
</code></pre>
<pre><code class="language-javascript">
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);
</code></pre>
<h3>طیف رنگ مخروطی</h3>
<p>طیف رنگ مخروطی یا conic-gradient نوعی طیف رنگ جدید است که هنوز در مرحله‌ی آزمایشی قرار دارد و پشتیبانی از آن بسیار ضعیف است؛ اما در اینجا فقط به بررسی مختصری از آن می‌پردازیم. توجه کنید، از آنجایی که این ویژگی هنوز در مرحله‌ی آزمایشی قرار دارد، ممکن است در آینده رفتار یا ورودی‌های متد آن تغییر کنند. این نوع طیف توسط متد <code>createConicGradient</code> ساخته می‌شود که سه ورودی می‌پذیرد: زاویه‌ی آغازین و مختصات مرکز.</p>
<p>به نمونه کد زیر دقت کنید. در کد زیر که شبیه به کد طیف رنگ خطی است، یک طیف رنگ مخروطی در مرکز canvas و با زاویه‌ی آغازین 90 درجه (نصف π رادیان) ساخته شده و رنگ‌های زرد و سبز به آن اضافه می‌شوند. برای اجرای این کد نیاز به مرورگری دارید که از این متد پشتیبانی کند. پس نگاهی به <a href="https://caniuse.com/mdn-api_canvasrenderingcontext2d_createconicgradient">جدول پشتیبانی</a> آن بیاندازید:</p>
<pre><code class="language-javascript">
ctx.createConicGradient(angle, X, Y);
</code></pre>
<pre><code class="language-javascript">
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);
</code></pre>
<p>از آنجایی که امکان تغییر رنگ برخی از خطوط در یک شکل وجود ندارد، طیف‌های رنگ می‌توانند جایگزین مناسبی برای اینگونه موارد باشند. هرچند پویایی چندانی ندارند، اما از هیچ بهترند! در بخش‌های آینده بیشتر با کاربرد طیف‌های رنگ آشنا خواهید شد.</p>
<h2>سایه (shadow)</h2>
<p>درست شبیه به CSS، در canvas امکان ایجاد سایه وجود دارد؛ البته با این تفاوت که فقط می‌توان یک سایه ایجاد کرد و مانند CSS نمی‌توان چندین سایه‌ی مختلف برای شکل تعریف کرد. سایه توسط چهار ویژگی تعیین می‌شود:</p>
<ul>
<li>ویژگی <code>shadowColor</code>: این ویژگی رنگ سایه را تعیین می‌کند. مقدار پیش‌فرض آن <code>rgba(0,0,0,0)</code> است و هر نوع رنگی را (به جز طیف رنگ و الگو) می‌پذیرد.</li>
<li>ویژگی <code>shadowBlur</code>: این ویژگی مات بودن سایه را تعیین می‌کند. مقدار پیش‌فرض آن 0 است و هر مقدار مثبتی را می‌پذیرد. هر چه این مقدار بزرگ‌تر باشد، میزان مات بودن سایه بیشتر خواهد بود.</li>
<li>ویژگی <code>shadowOffsetX</code>: این ویژگی فاصله‌ی سایه از شکل را در جهت محور X‌ها تعیین می‌کند. یعنی اگر مقدار آن 100 باشد، سایه 100 واحد به سمت راست شکل خواهد بود. مقدار پیش‌فرض این ویژگی 0 است.</li>
<li>ویژگی <code>shadowOffsetY</code>: این ویژگی فاصله‌ی سایه از شکل را در جهت محور Yها تعیین می‌کند. یعنی اگر مقدار آن 100 باشد، سایه 100 واحد پایین‌تر از شکل خواهد بود. مقدار پیش‌فرض این ویژگی 0 است.</li>
</ul>
<p>هر شکلی که در canvas رسم می‌شود، دارای سایه است؛ اما به خاطر مقدار‌های پیش‌فرض این ویژگی‌ها، سایه دقیقا پشت شکل افتاده و به نظر می‌رسد که شکل‌های رسم‌شده سایه ندارند. سایه‌ها ویژگی‌هایی دارند که در ادامه به بررسی آن‌ها می‌پردازیم.</p>
<p>به نمونه کد زیر دقت کنید. در کد زیر سایه به رنگ سبز، و در هر محور به فاصله‌ی 10 از شکل و به اندازه‌ی 5 واحد مات است. رنگ خود شکل را نیز آبی می‌کنیم. برای یادگیری بیشتر و تسلط بهتر به موضوع، کد را تغییر داده و تغییرات را بررسی کنید:</p>
<pre><code class="language-javascript">
ctx.shadowOffsetX = 10;
ctx.shadowOffsetY = 10;

ctx.shadowBlur = 5;
ctx.shadowColor = "#32CD32"; /* GREEN */

ctx.fillStyle = "#008DDE"; /* BLUE */
ctx.fillRect(100, 100, 200, 200);
</code></pre>
<p>به ازای هر پیکسلی که رسم می‌شود، یک سایه نیز رسم می‌شود، این یعنی سایه‌ای که با متد <code>stroke</code> رسم می‌شود، با سایه‌ای که با متد <code>fill</code> رسم می‌شود متفاوت است. همچنین، اگر یک تصویر دارای شفافیت باشد، بخش‌های شفافش دارای سایه نخواهند بود، زیرا بخش‌های شفاف رسم نمی‌شوند که سایه داشته باشند! در کد زیر یک <code>canvas</code> با ترسیماتی درون خود ایجاد می‌شود، سپس در یک <code>canvas</code> دیگر که دارای سایه برای ترسیمات است، رسم می‌شود. هنگام رسم، فقط به آن بخش‌هایی از تصویر سایه داده می‌شود که شفاف نیستند. کد را اجرا کرده و نتیجه را ببینید:</p>
<pre><code class="language-javascript">
let cvs_img = (() =&gt; {
        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);
</code></pre>
<p>در واقع این ویژگی نتیجه‌ی یک ویژگی دیگر مربوط به سایه‌هاست. اگر پیکسلی که در حال رسم شدن است دارای شفافیت باشد، سایه نیز همان شفافیت را خواهد داشت. برای درک بهتر موضوع کد زیر را درنظر بگیرید. در کد زیر ویژگی <code>fillStyle</code> دارای شفافیت 0.1 است ولی سایه هیچ شفافیتی ندارد، اما همانطور که خواهید دید این شفافیت روی سایه نیز اثر می‌گذارد:</p>
<pre><code class="language-javascript">
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);
</code></pre>
<p>این ویژگی باعث شده شفافیت رنگ مستقیما روی سایه اعمال شود. به این ترتیب یک پیکسل کاملا شفاف (یا دارای شفافیت 0) چه سایه‌ای خواهد داشت؟ یک سایه‌ی کاملا شفاف! یا سایه‌ای که دیده نمی‌شود. به این ترتیب بخش‌های شفاف تصویر یا شکل سایه ندارند.</p>
<p>سایه‌ها بخشی از شکل فعلی نیستند اما وارد لایه‌ی ترسیمات می‌شوند. درضمن تا زمانی که ویژگی‌های مربوط به سایه‌ها به مقدار پیش‌فرض خود بازنگردند، برای شکل‌های جدید سایه رسم خواهد شد. نکته‌ی آخر اینکه رسم سایه به پردازش نسبتا بالایی نیاز دارد، بنابراین بهتر است از آن در برنامه‌های سنگین کمتر استفاده کنید تا سرعت برنامه پایین نیاید.</p>
<p>به جز موارد گفته‌شده، باید اشاره‌ای نیز به متد <code>setShadow</code> داشته باشیم. این متد سال‌ها پیش توسط مرورگر‌های رده webkit پشتیبانی می‌شد اما مدت‌هاست که منقضی شده و استفاده از آن اصلا پیشنهاد نمی‌شود. برای راحتی کار با سایه‌ها در بخش «شخصی‌سازی canvas» راه‌هایی بررسی شده.</p>
<h2>نتیجه‌گیری</h2>
<p>در این بخش سعی کردیم چهار ویژگی مهم در canvas را مورد بررسی قرار داده و ویژگی‌های خاص هرکدام را توضیح دهیم. ممکن است موارد گفته‌شده کاربرد زیادی در ترسیمات نداشته باشند، اما در جای خود بسیار کاربری هستند و ترسیمات خیلی زیبایی می‌سازند، برای نمونه تصویر بالای پست با این ویژگی‌ها ساخته شده. همچنین در یکی از آموزش‌های پایانی کاربرد آن‌ها را در عمل خواهید دید. در آموزش بعدی گذری کوتاه بر متن خواهیم داشت.</p>
<p>نوشته <a rel="nofollow" href="https://css-tricks.ir/tutorial/%d8%ae%d8%b7%e2%80%8c%da%86%db%8c%d9%86%d8%8c-%d8%a7%d9%84%da%af%d9%88%d8%8c-%d8%b7%db%8c%d9%81-%d8%b1%d9%86%da%af%d8%8c-%d9%88-%d8%b3%d8%a7%db%8c%d9%87-%d8%af%d8%b1-canvas/">خط‌چین، الگو، طیف رنگ، و سایه در canvas</a> اولین بار در <a rel="nofollow" href="https://css-tricks.ir">آموزش طراحی وب</a> پدیدار شد.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.ir/tutorial/%d8%ae%d8%b7%e2%80%8c%da%86%db%8c%d9%86%d8%8c-%d8%a7%d9%84%da%af%d9%88%d8%8c-%d8%b7%db%8c%d9%81-%d8%b1%d9%86%da%af%d8%8c-%d9%88-%d8%b3%d8%a7%db%8c%d9%87-%d8%af%d8%b1-canvas/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
