تبدیلات transform در canvas

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

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

تغییر اندازه (scale)

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


ctx.scale(scale_x. scale_y);

به نمونه کد زیر توجه کنید. در کد زیر با استفاده از این متد اندازه‌ی ترسیمات در راستای محور X‌ها 2 برابر می‌شود. به همین دلیل مربعی که تعریف کرده‌ایم به شکل مستطیل رسم می‌شود. کد را تغییر داده و تغییرات آن را مشاهده کنید:


ctx.scale(2, 1);

ctx.fillStyle = "#0AF";
ctx.fillRect(50, 50, 100, 100);

در کد بالا یک مربع در مختصات (50,50) و به طول ضلع 100 تعریف شد، اما به خاطر تغییر اندازه، یک مستطیل در مختصات (100,50) و در ابعاد 200 × 100 رسم شد. نکته‌ی مهم اینکه می‌توان به تعداد دلخواه این متد را فراخوانی کرد، اما باید بدانید که ورودی‌های این متد در اندازه‌ی قبلی ضرب می‌شوند. یعنی در کد زیر ترسیمات نهایی سه‌برابر نمی‌شوند، بلکه شش برابر می‌شوند. به این دلیل که ورودی‌های متد دوم در متد اول ضرب می‌شوند:


ctx.scale(2, 2);
ctx.scale(3, 3);

/* 6 TIMES LARGER */

انتقال (translate)

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


ctx.translate(translate_x, translate_y);

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


ctx.translate(200, 200);
ctx.arc(0, 0, 150, 0, Math.PI * 2);

ctx.fillStyle = "#CC0";
ctx.fill();

نمونه کد زیر مبدا مختصات را به مرکز عنصر آورده و راستای محور Y‌ها را نیز قرینه می‌کند. این کد هنگام رسم نمودار‌ها، به‌ویژه نمودار‌های ریاضی کاربردی خواهد بود. این کد را در ترسیمات خود قرار داده و تغییرات آن را مشاهده کنید:


ctx.translate(cvs.width / 2, cvs.height / 2);
ctx.scale(1, -1);

همانند متد قبل، می‌توانید این متد را به تعداد دلخواه فراخوانی کنید، اما باید توجه داشته باشید که ورودی‌های این متد، با ورودی‌های پیشین جمع می‌شوند. این یعنی در کد زیر مبدا مختصات به جای (250,100) در (350,50) خواهد بود، زیرا ورودی‌های جدید، با ورودی‌های پیشین جمع می‌شوند:


ctx.translate(100, -50);
ctx.translate(250, 100);

/* ORIGIN AT (350, 50) */

چرخش (rotate)

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


ctx.rotate(angle);

شبیه به متد‌های قبل، می‌توانید این متد را به تعداد دلخواه و با هر مقدار عددی به عنوان ورودی فراخوانی کنید، اما باید توجه داشته باشید که زاویه‌ی ورودی با زوایای قبلی جمع می‌شود. یعنی در کد زیر، چرخش نهایی 120 درجه است، نه 90 درجه!


ctx.rotate(Math.PI / 6); /* 30 DEGREES */
ctx.rotate(Math.PI / 2); /* 90 DEGREES */

/* ROTATION IS 120 DEGREES */

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


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

در کد بالا متد scale با ورودی‌های 1 نیز نوشته شده. هر کدام از ورودی‌ها را به -1 تغییر دهید تا تاثیر آن را بر جهت چرخش مربع ببینید. همچنین می‌توانید مختصات مربع را تغییر دهید تا درک بهتری از مفهوم «مرکز چرخش» به دست آورید. هرچند در کد بالا از تابع setInerval برای ایجاد انیمیشن استفاده شد، اما این تابع برای این هدف مناسب نیست. در آموزش‌های آینده توابع مناسب‌تر بررسی خواهند شد.

ماتریس تبدیلات

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

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

هنگام استفاده از یکی از متد‌های تبدیلات، یک ماتریس جدید تعریف می‌شود. این ماتریس اطلاعات مربوط به متد را درون خود دارد. مثلا اگر متد انتقال با ورودی (200,300) فراخوانی شود، یک ماتریس با این اطلاعات ایجاد می‌شود. سپس این ماتریس، در ماتریس اصلی canvas ضرب می‌شود. وقتی دو ماتریس تبدیلات در یکدیگر ضرب می‌شوند، ماتریس نتیجه، ترکیبی از هر دو تبدیل خواهد بود. برای درک بهتر به شبه‌کد زیر توجه کنید:


{rotate: 45} × {scale: (2,3)} = { rotate: 45 , scale: (2,3) }
{scale: (2,3)} × {rotate: 45} = { scale: (2,3) , rotate: 45 }

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

متد transform

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


ctx.transform(scale_x, skew_y, translate_x, scale_y, skew_x, translate_y);

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

متد setTransform

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

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


ctx.setTransform(1, 0, 0, 1, 0, 0);

متد resetTransform

این متد که هنوز در مرحله‌ی آزمایشی قرار دارد، ماتریس اصلی canvas را به حالت اولیه باز می‌گرداند:


// Reset transformation matrix to the identity matrix
ctx.resetTransform();

البته پشتیبانی از این متد به خوبی setTransform نیست و پیشنهاد می‌شود به جای این متد از setTransform استفاده کرده یا حداقل از کد زیر برای ایجاد پشتیبانی از این متد استفاده کنید:


if (!CanvasRenderingContext2D.prototype.resetTransform) {
    CanvasRenderingContext2D.prototype.resetTransform = function () {
        this.setTransform(1, 0, 0, 1, 0, 0);
    }
}

مکان مهم است!

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


ctx.rect(50, 50, 150, 150);

ctx.scale(2, 1);
ctx.rect(350, 50, 100, 100);

ctx.fillStyle = "#3D3";
ctx.fill();

با اجرای کد بالا متوجه می‌شوید که مربع اول از متد scale تاثیر نگرفته، زیرا پیش از آن متد تعریف شده. اگر این متد پیش از مربع اول فراخوانی می‌شد، روی هر دو شکل تاثیر می‌گذاشت.

ترتیب مهم است!

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

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


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

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

در گام اول، کد را بدون هیچ تبدیلی اجرا می‌کنیم. برای گام اول می‌توان از رنگ سیاه استفاده کرد. دو تابع clear و draw_shape را به کد اصلی خود اضافه کرده و کد‌هایی که در ادامه بررسی می‌شوند را اجرا کنید.

آزمایش اول، تبدیلات ساده

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


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

آزمایش دوم، انتقال و چرخش

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


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

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

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

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

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


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

نتیجه‌گیری

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

حسین رفیعی

حسین رفیعی

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

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

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