شبیه ساز ماتریکس LED
چند وقت پیش یه دستگاه جذاب دیدم که برای رامین فرستادم و ازش پرسیدم آیا همچین نوع صفحه نمایشی بیرون هست که بخوام براش برنامهنویسم خودم و این دستگاه رو نخرم؟ رامین پیشنهاد داد که یه صفحه 32x16 ماتریکسی RGB بگیرم و از اون شروع کنم ساختن چیزی که میخوام. اما با توجه به اینکه از خرید تا رسیدنش دستم کمی طول میکشید، تاب نیاوردم و تصمیم گرفتم شبیه ساز همین صفحه رو با جاواسکریپت بسازم.
Dom یا Canvas
تلاش اول رو با Dom انجام دادم. از ری-اکت استفاده کردم که ۱۶ ردیف ۳۲ تایی div مختلف بچینم کنار هم و بر اساس یه آرایه اونهایی که میخوام رو رنگآمیزی کنم که شبیه اون صفحه در بیاد. همه چیز خوب بود تا اینکه تصمیم گرفتم به انیمیشن تبدیلش کنم. الگوریتم انیمیشن اینه: دادههای صفحه یه آرایه ۱۶ تایی هست که هر کدوم از اعضاش، یه آرایه ۳۲ تایی از آبجکتهایی هستن که توش وضعیت پیکسل (خاموش و روشن) و رنگ اون رو مشخص میکنن. برای اینکه تبدیل به انیمیشن بشه، برای هر فریم از انیمیشن، یکی از اون اعضای آرایه ۳۲ تایی رو از اول آرایه برمیداریم و میذاریمش ته آرایه، و این کارو برای هر ۱۶ عضو آرایه اصلی تکرار میکنیم و بعد تابعی که اون div هارو رنگ میکنه دوباره صدا میزنیم.
جاواسکریپت با سرعت میتونه اون آرایهها رو پردازش کنه و تغییرشون بده، ولی تغییر دادن استایل ۵۱۲ تا div در Dom خیلی کار سریعی نیست. برای همین نمیشه در هر ثانیه ۶۰ بار این کار رو انجام داد (۶۰ فریم بر ثانیه که هدف منه). نهایتا میشه در هر ثانیه ۲۰ تا ۳۰ بار این کار رو انجام داد و بخاطر همین انیمیشن نهایی روون نمیشه.
بنابراین تصمیم گرفتم که به جای Dom از Canvas استفاده کنم چون خیلی سریعتره. تغییر دیگهای که فکر کردم بدم بهتره این بود که به جای آرایه ۲ بعدی برای رنگآمیزی صفحه از آرایه یک بعدی استفاده کنم. یعنی به جای این:
const arr = [ [{on: true}, {on: false}, {on: true}, ...], [{on: true}, {on: false}, {on: true}, ...], [{on: false}, {on: true}, {on: false}, ...], ... ];
از این استفاده کنم:
const arr = [ {on: true}, {on: true}, {on: true}, {on: true}, {on: true}, {on: true}, ... ];
ولی با این روش، دسترسی به x و y برای پر کردن خونهها سختتر میشه. چون در حالت ۲ بعدی اگر مثلا بخوام به اطلاعات پیکسل x=10
و y=5
مثلا دسترسی پیدا کنم، راحت مینویسم arr[5][10]
و برای رسم همه پیکسلها روی صفحه به دو تا حلقه for نیاز هست. ولی در حالت یک بعدی، برای رسم پیکسلها، باید تو یه حلقه for
مقدار y
رو با Math.floor(i / 32)
و مقدار x
رو با i - (y * 32)
به دست بیارم. برای دسترسی به یه x
و y
خاص هم باید اون رو با (y * 32) + x
محاسبه کرد. ولی خب در نهایت محاسبهها سریعتر میشن چون فقط یه حلقه for
به اندازه x * y
داریم، نه یه آرایه دو بعدی و دو تا حلقه. (البته برای یه صفحه 32x16 این تفاوت سرعت اصلا محسوس نیست).
با این تغییرات (در اصل تغییر Dom به Canvas) و استفاده از requestAnimationFrame
خیلی راحت ۶۰ فریم در ثانیه به دست میاد.
نوشتن متن روی صفحه
میکروکنترلرها برای اینکه متن رو روی اسکرینهای پیکسلی بنویسن، از فونتهای Bitmap استفاده میکنن. فونتهای بیتمپ همونطور که از اسمشون مشخصه، فونتهایی هستن که اطلاعات هر گلیف (حرف) رو به صورت یه آرایه از پیکسلها ذخیره میکنن. مثلا اگر عرض هر حرف قراره ۶ پیکسل باشه، و ۹۶ حرف داشته باشیم، یه فونت بیتمپ در واقع یه آرایه دو بعدی ۹۶در۶ هست. هر پیکسل به صورت یه بایت اطلاعات ذخیره شده (هشت بیت) که در واقع ارتفاع پیکسلی اون ستون هست. بنابراین هر گلیف اندازش ۶ در ۸ پیکسله. مثلا این اطلاعات حرف A هست:
[0x7c,0x24,0x24,0x24,0x7c,0x00]
شیش تا عدد هگزادسیمال (مبنای ۱۶). حالا این رو چطور رسم کنیم؟ اول اون عددای هگزادسیمال رو تبدیل به باینری میکنیم (مبنای دو). توی جاواسکریپت این کار به این سادگی انجام میگیره. ولی ما نیاز به یه بایت اطلاعات داریم و این تبدیل شامل صفرهای اول بایت نمیشه پس باید خودمون صفرهاشو بذاریم. هرکدوم از این صفر و یک ها در این بایت، اطلاعات ۸ پیکسل تو ستون اول گلیف ما هستن. و اگر شیش تا از این ستونها رو کنار هم بذاریم حرف A شکل میگیره. نکته دیگه اینه که میکروکنترلرها از پایین به بالا میچینن ولی ما از بالا به پایین، برای همین باید اون بایت رو برعکس کنیم.
{{ img led-simulator/drawing.png "drawing" "نقشه" }}
موضوع بعدی اینه که چطوری اطلاعات هر حرف رو از توی فونت پیدا کنیم؟ آرایه اصلی فونت به ترتیب عدد یونیکد هر حرف ذخیره شده. مثلا گلیف فاصله (کد یونیکد ۳۲) اولیه (صفرمی در واقع)، علامت تعجب (کد ۳۳) دومی (اولی در واقع) و الی آخر. پس برای پیدا کردن گلیف، باید کد یونیکد هر حرف رو به دست بیاریم، ازش ۳۲ تا کم کنیم و اون عدد نهایی، ایندکس گلیف تو آرایه فونته. بعد به ترتیب اینا رو کنار هم با فرمولی که توی بخش بالا گفتم رسم میکنیم :)
ابزار
برای نوشتن کد این شبیهساز به جای جاواسکریپت معمولی از تایپاسکریپت استفاده کردم بخاطر اینکه توی همچین کدی تایپها نقش مهمی رو دارن و اگر یه چیز اشتباهی جای چیز دیگهای باشه همه چی بهم میریزه و پیدا کردن مشکل کمی سخت میشه. دلیل دیگهای هم داشت و اونم اینه که دوست داشتم یه پروژه شخصی رو هم با این زبون انجام بدم.
برای پیاده سازی مثالها و بقیه چیزهای نمایشی هم به جای ریاکت از preact استفاده کردم که نسخه ۳ کیلوبایتی ریاکت هست در واقع و خیلی شبیهه و کمی سریعتر. اونم فقط به این خاطر که میخواستم از چیز جدید و سبک استفاده کنم.
کدهای این شبیهساز رو اینجا و مثال زندهاش رو رو میتونید اینجا ببینید و باهاش بازی کنید.