چطور با جاوا اسکریپت یک Particle System باحال بسازیم #1

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

چند وقت پیش داشتم توی گیتهاب میچرخیدم که به همچین پروژه ای برخورد کردم:

https://github.com/VincentGarreau/particles.js/

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

https://github.com/Amohammadi2/particles-effect

قراره یه همچین چیزی بسازیم این بار ?

من که خیلی چیزا از انجام دادن این پروژه یاد گرفتم و امیدوارم برای شما هم همینطور باشه. در این آموزش قرار نیست هیچ فریمورک جاوا اسکریپتی استفاده بشه و برای سادگی کار صرفا از جاوا اسکریپت وانیلی (vanilla JavaScript) استفاده خواهد شد.

شروع پیاده سازی

خوب برای شروع به یکم html نیاز داریم:

که خوب چیز خاصی هم نیست. فقط یه canvas داره و بقیه اش چیزای معمولیه که توی هر صفحه html ای هست. راستی این canvas ما یه سری style هم داره:

اول از همه برای اینکه margin یا padding های default مزخرفی که مرورگر ها برای body یا کلا تگ html ممکنه در نظر بگیرن رو از بین ببریم، اون ها رو روی 0 تنظیم می کنیم.

بعد هم canvas مون رو full screen میکنیم و background اش رو به رنگ مشکی درمیاریم، همین.

حالا که پیش نیاز های پروژه مون حاضره می تونیم بریم سراغ اصل کار، یعنی javascript!

آماده سازی بوم نقاشی! (canvas)

قبل اینکه شروع کنیم، باید یه سری کار انجام بدیم که canvas مون ردیف بشه.

تعريف ثوابت global لازم

قبل پیاده سازی، اول بزارید global هامون رو تعریف کنیم:

قراره از ctx خیلی استفاده کنیم. ctx بهمون دسترسی به یه سری API برای نقاشی آزادانه روی canvas رو میده. می تونم بگم که با method هایی که داره هرچیزی که تو ذهنتون بگنجه میشه رسم کرد، حتی اگه بلد باشید می تونید گرافیک سه بعدی رسم کنید (البته ریاضیاتش دیگه با خودتونه)

سینک نگه داشتن طول و عرض canvas

وقتی که پنجره مرورگر resize میشه، کلا مقیاس canvas مون به هم می ریزه و کار خراب میشه، پس مهمه که هم طول و هم عرض canvas رو با طول و عرض پنجره مرورگر sync نگه داریم. برای اینکار چنین تابعی تعریف می کنیم که کمکمون کنه:

و این چنین ازش به عنوان event listener استفاده می کنیم:

پیاده سازی کلاس Particle

قراره از این کلاس برای ساختن particle استفاده کنیم. این کلاس قادره که particle رو رسم کنه و حرکت بده. خوب اول بریم سراغ constructor اش

خوب، همون طور که میبینید constructor چهار تا پارامتر دریافت می کنه، اول از همه موقعیت اولیه particle روی محور x و y در canvas و بعد شعاع دایره و در نهایت direction یا همون جهت حرکت particle رو دریافت میکنه. می تونید تا به اینجا، نحوه ایجاد object از روی این کلاس رو اینطور فرض کنید:

new Particle(100,  200, 0.5, {
x: 10
y: 4
});

ترسیم دایره

برای رسم دایره، یک متد به نام draw ایجاد میکنیم که قراره از پارامتر های x، y و radius استفاده کنه تا دایره رو رسم کنه:

حالا بزارید دقیق و خط به خط بهتون توضیح بدم که چه اتفاقی داره میوفته:

ctx.beginPath();

مثل این هست که بگیم قلم رو بزار رو کاغذ

ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);

بعد یک کمان 360 درجه رسم کن

توضیحات بیشتر راجع به پارامتر ها:

  • در این قسمت، x و y مختصات مرکز کمان دایره هستند
  • پارامتر سوم (this.radius) میشه شعاع کمان
  • پارامتر چهارم، زاویه شروع ترسیم کمان هست. وقتی میگیم زاویه 0، منظورمون زاویه 0 درجه دایره مثلثاتی هست که ترسیم کمان باید از اونجا شروع بشه
  • پارامتر پنجم هم زاویه اتمام ترسیم کمان هست. از اونجایی که می خواهیم یه کمان 360 درجه رسم کنیم (دایره)، مقدار این پارامتر باید برابر با 2Pi باشه، چون واحد هر دو زاویه شروع و پایان، برحسب رادیان هست. برای اینکه متوجه بشید که چطور به 2Pi رسیدیم، انیمیشن زیر رو ببینید:
منبع: ویکی پدیا عزیز
منبع: ویکی پدیا عزیز
  • پارامتر آخر هم که جهت ترسیم کمان رو مشخص میکنه، اگه false باشه کمان ساعتگرد و اگه true باشه کمان پادساعتگرد رسم میشه (که در این مورد زیاد فرقی نمی کنه true باشه یا false چون داریم دایره رسم میکنیم)

خوب، بریم سراغ خط بعدی:

ctx.fillStyle = &quot#fff&quot

در اینجا داریم رنگ دایره رو تعیین میکنیم (fillStyle رنگ داخل دایره است)، اما این فقط انتخاب رنگ قلم هست، برای اینکه اعمال بشه باید fill رو فراخوانی کنیم:

ctx.fill();

حالا دایره رنگ شد ?

ctx.closePath();

این هم مثل این هست که بگیم "لطفا قلم رو از روی canvas بردار". اگه این کار رو نکنیم همه دایره ها با خط هایی به هم وصل میشن، طوری که انگار هر دایره ادامه قبلیه. دقیقا مثل وقتی که میخواهید اسم و فامیلتون رو بدون برداشتن خودکار از روی کاغذ بنویسید.

سربارگذاری اولیه particle (همون initialization)

این کار ضروری نیست ولی من کرم دارم و می خوام یکم پیچیده اش کنم:

وقتی این متد از کلاس Particle رو فراخوانی کنید، particle فقط رسم میشه اما موقعیت و ... آپدیت نمیشن، این رو فقط برای رسم particle در frame اول نیاز داریم. همین (حتی اگه می خواهید می تونید حذفش کنید تاثیر خاصی نمیزاره)

حرکت particle ها

بریم یه تکونی به particle های عزیز بدیم. برای اینکار متدی به نام update میسازیم:

با آپدیت کردن مقادیر x و y به صورت پیوسته، particle رو حرکت میدیم و البته حواسمون هم هست که از canvas نزنه بیرون. اگه از سمت راست بخواد بزنه بیرون x > cnvs.width میشه، اگه از سمت چپ بخواد بزنه بیرون x < 0 میشه، اگر از سمت پایین بخواد بزنه بیرون y > cnvs.height میشه، اگر هم بخواد از بالا بزنه بیرون y < 0 میشه. پس طبق چیزایی که گفتم، چنین شرط هایی رو میزاریم که جهت حرکت رو در هر حالت برعکس کنند، اینطوری particle به هر طرفی برخورد کنه، در خلاف همون جهت برمیگرده. (مثلا وقتی به دیواره بالا بخوره جهتش برمیگرده رو به پایین و ...):

تعریف یه سری ثوابت دیگه

  • در اولی قراره خود particle ها ذخیره بشن، چون قراره در تمام طول اجرا شدن effect کلی باهاشون پردازش انجام بدیم.
  • دومی تعداد particle هایی که می خواهیم داشته باشیم رو مشخص می کنه (هرمقدار دلخواهی می تونه باشه)
  • سومی هم سرعت particle هاست، واحدش هم نمی دونم چیه خودتون باهاش وربرید تا به یه سرعت مناسب برسید.

ایجاد و ترسیم Particle ها

حالا که کلاس Particle تکمیل شده، وقتشه که ازش استفاده کنیم:

نتایج فعلی

تاحالا هیچ چیز خاصی اتفاق نیوفتاده، فقط یه سری نقاط ثابت روی صفحه خواهیم داشت:

انگار آسمون شبه
انگار آسمون شبه

حرکت دادن نقاط

برای update کردن موقعیت particle ها به طول مداوم، تابعی مثل این تعریف می کنیم. علت این که این قسمت رو جدا نوشتیم اینه که می خواهیم از requestAnimationFrame استفاده کنیم که اطلاعات مربوط به اون رو در این لینک می تونید مشاهده کنید

در این تابع، ما اول با clearRect میایم و کل canvas رو پاک می کنیم تا نتایج قبلی پاک بشن و جدید ها جای اون ها رو بگیرن. بعد هم موقعیت particle ها رو یکی یکی update میکنیم و با اینکار حرکتشون میدیم. همین

ما باید مدام playParticles رو فراخوانی کنیم. اون طور که من خودم امتحان کردم، با حلقه های بی نهایت به جایی نمی رسید. مرورگر به خاطر بلاک شدن thread توسط شما گیر می ده و کاربر رو مجبور میکنه که مرورگر رو ببنده. در ضمن، در این مدت هیچ کد جاوا اسکریپت دیگه ای نمی تونه اجرا بشه. با recursive call هم می تونید فوقش 10000 با تابع رو به طور recursive فراخوانی کنید، بعدش دیگه جاوا اسکریپت نمیزاره ادامه بدید و ارور میده ... تنها راهی که وجود داره برای اجرای پشت سر هم و بی نهایت یه سری کد، همین تابع requestAnimationFrame هست. نه مرورگر بهتون گیر میده، نه صفحه وب هنگ می کنه و نه performance پایین میاد (البته این مورد نهایی بستگی به نحوه کد نوشتن خودتون هم داره)

نتایج فعلی

الان اون نقاط ثابت درحال حرکت هستند:

الان شده اند مثل ریزگرد های تو هوا
الان شده اند مثل ریزگرد های تو هوا

ترسیم خط بین particle های نزدیک

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

برای این کار، تابعی به نام lineToNearParticles رو تعریف می کنیم که قراره یک particle رو بگیره و به particle هایی که در نزدیکی شعاعش هستند خط ترسیم کنه:

اول particle رو میگیریم و یکی یکی میریم سراغ بقیه particle ها و فاصله particle مون رو با تک تک particle های دیگه محاسبه می کنیم. توجه کنید که این محاسبات خیلی زیاده (فکر کنم پیچیدگی O(n^2) داشته باشه) و نمی تونید از یه حدی بیشتر particle تعریف کنید وگرنه انیمیشن تون هنگ میکنه

محاسبه فاصله بین دو particle از طریق رابطه فیثاغورس حساب شده. به این صورت که اول اختلاف مختصات x شون و بعد اختلاف مختصات y شون محاسبه میشه، سپس از طریق جذر گرفتن از مجذور مجموع این دو مقدار، فاصله مستقیم دو نقطه حاصل میشه. اطلاعات بیشتر در این صفحه

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

r1 + r2 > d

که در اینجا r1 و r2 به ترتیب، شعاع particle اول و دوم هستند و d هم فاصله مستقیم بین این دو particle رو نشون میده. خوب اگه مجموع شعاع هاشون از فاصله شون بیشتر بشه، شعاع هاشون هم به هم برخورد میکنه. خوب از طریق اصل بالا بقیه کد رو می نویسیم:

تنها نکته ای که به ذهنم میرسه اینه که اگر متوجه شده باشید، strokeStyle و stroke به جای fillStyle و fill استفاده شده، دلیلش هم اینه که خط مساحت نداره که توش رو بخواهیم با fill رنگ کنیم. برای رنگ کردن خط باید از stroke استفاده کنیم (در اشکال دوبعدی، این کار باعث رنگ شدن محیط شکل میشه (همون اضلاع))

استفاده از اون در تابع playParticles

و حالا نتیجه میشه این:

خوب، دقیقا اون چیزی که می خواستیم نشده هنوز. خط ها هیچ کدوم انیمیشن ندارن و همه چی خشک و خالیه و هیچ interaction ای وجود نداره اما تا همینجاش کافیه. نمی خوام بیشتر از این مطلب طولانی بشه.


در نوشتن این مطلب این طور فرض کردم که مخاطب یک فرد مبتدی هست و سعی کردم تا جایی که توانش رو داشتم راجع به همه جزئیات توضیح بدم و تا جایی که میشد مطالب رو واضح بیان کنم. اگر این مطلب براتون مفید بود، با یه لایک به من انرژی بدید که انشاالله سری دوم این مقاله رو هم زودتر بنویسم.

تا مقاله بعدی فعلا خدانگهدار!