متد‌های save و restore در canvas

در این آموزش به یکی از بهترین و پرکاربرد‌ترین امکانات canvas می‌پردازیم. متاسفانه (یا خوشبختانه) گستردگی این دو متد آنقدر بالاست که یادگیری آن‌ها نیازمند دانش نسبتا بالایی از canvas است و لازم است پیش از یادگیری آن‌ها، تقریبا تمام آموزش‌های پیشین را به خوبی آموخته باشید.

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

متد save

این متد یک آرایه از اطلاعات فعلی canvas ذخیره می‌کند. این متد هیچ ورودی‌ای نداشته و به تعداد دلخواه می‌تواند فراخوانی شود و فارغ از اینکه آرایه‌ی اطلاعات تکراری است یا خیر، موارد زیر را ذخیره می‌کند:

  • ماتریس تبدیلات فعلی
  • محدوده‌ی فعلی لایه‌ی ترسیمات (ویژه متد clip)
  • خط‌چین تعریف شده
  • مقدار تمام ویژگی‌های strokeStyle، fillStyle، globalAlpha، lineWidth، lineCap، lineJoin، miterLimit، lineDashOffset، shadowOffsetX، shadowOffsetY، shadowBlur، shadowColor، globalCompositeOperation، font، textAlign، textBaseline، direction، imageSmoothingEnabled

نکته‌ی قابل توجه اینکه ویژگی imageSmoothingEnabled تا حدودی شبیه به ویژگی image-rendering در CSS عمل می‌کند اما پشتیبانی آن نسبتا پایین است. درضمن ویژگی direction نیز شبیه به ویژگی direction در CSS عمل کرده و جهت متن را مشخص می‌کند، اما هنوز در مرحله‌ی آزمایشی قرار دارد و پشتیبانی از آن ضعیف است. به همین دلیل تاکنون به آن‌ها نپرداخته‌ایم.

متد restore

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


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 */

در کد بالا ابتدا ویژگی fillStyle به آبی تعیین می‌شود، و سپس متد save این حالت را ذخیره می‌کند. سپس ویژگی fillStyle به زرد تعیین شده و یک مربع رسم می‌شود. حال متد restore فراخوانی می‌شود که آخرین حالت ذخیره‌شده را اعمال می‌کند، یعنی ویژگی fillStyle دوباره آبی می‌شود. حال یک مربع دیگر رسم می‌شود.

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


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 < 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();

این کد نیاز به کمی توضیح دارد. ابتدا ویژگی‌های ابتدایی تنظیم می‌شوند. سپس سه متغیر grid، grid_unit، و size تعریف می‌شوند. متغیر grid اندازه‌ی جدول را تعیین می‌کند، متغیر grid_unit تعیین‌کننده‌ی فاصله‌ی هر عنصر جدول از دیگر عناصر است، و متغیر size نصف اندازه‌ی هر مربع است.

حال به کمک متد save حالت فعلی canvas ذخیره می‌شود تا بعدا استفاده شود. حال یک حلقه به اندازه‌ی توان دوم grid ایجاد می‌شود، زیرا تعداد اعضای جدول همین تعداد است. درون حلقه، متغیر‌های x و y تعیین می‌شوند. مقدار i % grid مقداری کوچکتر از grid است که می‌تواند مختصات افقی عنصر جدول را به ما بدهد، البته پیش از آن باید در مقدار مناسب ضرب شود. مقدار i / grid | 0 نیز عددی صحیح است که می‌تواند مختصات عمودی عنصر جدول را به ما بدهد.

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

حال اندازه‌ی حاشیه را به صورت تصادفی (وابسته به grid) تعیین کرده و یک رنگ تصادفی به فرمت HSL ایجاد می‌کنیم. کد مربوط به strokeStyle برای ایجاد رنگ تصادفی کاربرد فراوانی دارد، هرچند رنگ‌های آن محدود هستند، اما ساده‌ترین روش برای این کار به شمار می‌رود. حال یک مربع به مرکز مبدا مختصات رسم می‌کنیم. برای این کار مختصات آن باید در (-size,-size) و طول ضلع آن size * 2 باشد. حال که کار رسم تمام شد، تنظیمات قبلی canvas (پیش از چرخش و انتقال) را اعمال می‌کنیم.

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

شکل فعلی

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

ترتیب بازیابی اطلاعات

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


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();

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

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

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

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


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 < 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 < 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;

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

بخش 1 تعریف موارد ابتدایی

در این بخش ویژگی‌های lineCap، lineJoin، strokeStyle، و lineWidth تعیین می‌شوند. این ویژگی‌ها قرار نیست در طول برنامه تغییر کنند به همین دلیل در اینجا آن‌ها را تعیین می‌کنیم تا هنگام رسم راحت باشیم.

بخش 2 تعریف جدید تابع clear

این تابع همان کاربرد پیشین را دارد و کار آن پاک کردن لایه‌ی ترسیمات زمینه‌ی ورودی آن است. این تابع در این برنامه کاربردی ندارد و می‌توانید آن را حذف کنید. فقط از این جهت به این برنامه اضافه شده تا بتوانید کاربرد متد‌های save و restore را درون آن ببینید. به کمک این دو متد، دیگر نیازی نیست که مقدار‌های پیشین globalCompositeOperation و fillStyle ذخیره شوند.

بخش 3 تابع تولید عدد تصادفی

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

بخش 4 تابع ایجاد شکل تصادفی

این تابع وظیفه‌ی ایجاد یک شکل تصادفی را بر عهده دارد. این تابع پنج ورودی نیاز دارد که به ترتیب تعداد اضلاع شکل n، کران پایین اعداد تصادفی min، کران بالای اعداد تصادفی max، و مختصات مرکز شکل (rx,ry) است. به جای مختصات مرکز می‌توان از متد translate کمک گرفت. این تابع از مختصات قطبی برای ایجاد شکل استفاده می‌کند که برتری‌های خاص خود را دارد که در ادامه به آن‌ها خواهیم پرداخت.

بخش 5 متغیر‌های اولیه‌ی تابع

در این بخش متغیر‌هایی که درون حلقه استفاده می‌شوند، و دو متغیر vertices و angle تعریف می‌شوند. متغیر vertices یک آرایه است که قرار است مختصات نقاط شکل را داشته باشد، و متغیر angle نیز نسبت 2π به n است. اگر با مختصات قطبی آشنا باشید، به‌خوبی کاربرد این متغیر را می‌دانید. این متغیر برای یافتن زاویه‌ی درست مختصات در حلقه استفاده شود.

بخش 6 حلقه‌ی ایجاد نقاط

حال می‌خواهیم مختصات شکل را ایجاد کنیم. برای این کار یک حلقه به طول n ایجاد می‌کنیم. درون این حلقه یک مقدار تصادفی برای r ایجاد می‌کنیم. این متغیر حکم شعاع در مختصات قطبی را دارد. سپس به کمک این متغیر و زاویه‌ی فعلی i*angle مختصات دکارتی (یا همان X و Y) نقطه را به دست می‌آوریم. فرمول استفاده شده برای محاسبه‌ی این مقدار، در حال حاضر، نیاز به توضیح ندارد اما بعدا به آن خواهیم پرداخت. حال این مختصات (x,y) را در آرایه‌ی vertices ذخیره می‌کنیم.

بخش 7 مقدار خروجی تابع

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

بخش 8 تابع رسم شکل

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

بخش 9 پاک کردن شکل فعلی پیشین

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

بخش 10 رسم نقاط

در این بخش یک حلقه با گام 2 اجرا می‌شود که نقاط را به کمک متد lineTo رسم می‌کند. شاید این مورد به ذهن بیاید که «از آنجایی که شکل فعلی همین الان پاک شده، پس مختصات فعلی قلم تعریف‌نشده است و خط اول رسم نمی‌شود!» در واقع این کار از عمد انجام شده، زیرا قرار نیست خطی به نقطه‌ی اول رسم شود، بلکه فعلا باید قلم به آن نقطه برود. می‌توانستیم از تابع moveTo به صورت دستی استفاده کنیم ولی از تعریف‌نشده بودن مختصات قلم به نفع خودمان استفاده کردیم و یک کد کوتاه‌تر با همان کارکرد نوشتیم! در پایان حلقه، مختصات نهایی قلم روی نقطه‌ی آخر است، حال به کمک متد closePath نقطه‌ی آخر را به نقطه‌ی اول وصل می‌کنیم.

بخش 11 رسم شکل ایجاد‌شده

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

بخش 12 ایجاد سه شکل

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

بخش 13 تابع اصلی برنامه

این تابع هسته‌ی اصلی برنامه است و تمام ترسیمات درون این تابع انجام می‌شود. در ادامه به هر بخش درون این تابع می‌پردازیم.

بخش 14 پاک کردن ترسیمات و ذخیره‌ی اطلاعات فعلی

در ابتدای تابع لایه‌ی ترسیمات پاک شده و اطلاعات فعلی آن ذخیره می‌شوند. پاک کردن لایه‌ی ترسیمات ضروری نیست و می‌توانید این کد و تابع clear را از برنامه حذف کنید، اما اجرای متد save ضروری است، زیرا در ادامه قرار است از متد clip استفاده شود و اطلاعات فعلی برای بازیابی لازم هستند.

بخش 15 رسم شکل اول

در این بخش شکل درون متغیر red_shade درون شکل فعلی قرار می‌گیرد، سپس با متد clip ترسیمات به این شکل محدود می‌شوند. سپس به کمک ویژگی globalCompositeOperation یک افکت روی این تصویر اعمال می‌شود و تصویر درون این محدوده رسم می‌شود.

بخش 16 و 17 افکت‌های متفاوت

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

بخش 18 رسم تصاویر اصلی

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

بخش 19 ایجاد مدیریت رویداد

معمولا بدون نیاز به مدیریت رویداد می‌توان ترسیمات را انجام داد، اما در این مورد یک تصویر در صفحه وجود دارد و باید ابتدا منتظر بارگیری آن بمانیم. بنابراین یک مدیریت رویداد برای بارگیری (load) صفحه ایجاد می‌کنیم تا پس از بارگیری، تابع draw_flowers را اجرا کند.

مختصات قطبی

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

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


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 < 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 < 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 < 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();

canvas polar vs. cartesian coordinates

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

نتیجه‌گیری

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

حسین رفیعی

حسین رفیعی

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

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

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