**«تیفو»** همان طرح بزرگی است که تماشاگران با بالا بردن همزمان کارتهای رنگی روی جایگاه میسازند و **هر صندلی یک پیکسل از تصویر است!** این طرح از قبل روی یک شبکه کشیده میشود: هر سلول یک رنگ میگیرد و در روز بازی همان رنگ به دست تماشاگر آن صندلی میرسد. در این سوال باید همین ابزار طراحی را پیادهسازی کنید؛ ویرایشگری که با آن میشود روی صندلیها نقاشی کرد، لایهٔ متن و شکل اضافه کرد و دید **طرح** موقع اجرا چطور خانهبهخانه ظاهر میشود. منطق برنامه باید در ماژولهای جدا از هم نوشته شود و این ماژولها نباید به `DOM` دسترسی داشته باشند و رسم روی `canvas` انجام میگیرد. پیادهسازی کل این چالش با `JavaScript` است.

**هدف این سوال پیادهسازی مدلسازی داده و الگوریتمهای کار روی شبکه است**: ابزارهای نقاشی، لایهها، ناحیهٔ انتخاب و ترتیبهای نمایش تدریجی. ظاهر برنامه در ارزیابی نقشی **ندارد** و سیستم داوری کوئرا فقط خروجی ماژولها و رفتار برنامه را میسنجد.
# **پروژهٔ اولیه**
پروژهٔ اولیه را از [این لینک](/contest/assignments/103143/download_problem_initial_project/356840/) دانلود کنید.
<details class="green">
<summary>**ساختار فایلها و پروژه اولیه**</summary>
```plaintext
initial_project/
├─ <mark class="green" title="آماده است؛ تغییرش ندهید">index.html</mark>
├─ <mark class="green" title="آماده است؛ تغییرش ندهید">styles.css</mark>
└─ scripts/
├─ <mark class="orange" title="این فایل را تکمیل کنید">grid.js</mark>
├─ <mark class="orange" title="این فایل را تکمیل کنید">paint.js</mark>
├─ <mark class="orange" title="این فایل را تکمیل کنید">palette.js</mark>
├─ <mark class="orange" title="این فایل را تکمیل کنید">layers.js</mark>
├─ <mark class="orange" title="این فایل را تکمیل کنید">font.js</mark>
├─ <mark class="orange" title="این فایل را تکمیل کنید">select.js</mark>
├─ <mark class="orange" title="این فایل را تکمیل کنید">reveal.js</mark>
├─ <mark class="orange" title="این فایل را تکمیل کنید">playback.js</mark>
├─ <mark class="orange" title="این فایل را تکمیل کنید">templates.js</mark>
├─ <mark class="orange" title="این فایل را تکمیل کنید">history.js</mark>
├─ <mark class="orange" title="این فایل را تکمیل کنید">io.js</mark>
└─ <mark class="orange" title="این فایل را تکمیل کنید">main.js</mark>
```
+ **نکته:** داخل فایلهای پروژهٔ اولیه، در هر بخش کامنتهایی جهت انجام راهنمایی برای پیادهسازی قرار گرفتهاند: قرارداد هر تابع، حالتهای مرزی و نکتههای پیادهسازی. پیش از شروع هر فایل، کامنتهای بالای آن را بخوانید.
+ **نکته:** درخت بالا ساختار پروژه است. **دو فایل `index.html` و `styles.css` کامل و آماده در پروژهٔ اولیه هستند و نباید تغییرشان بدهید**؛ اگر همین حالا صفحه را باز کنید، کل استودیو با نوار ابزار و `Canvas` و پالت و پنل لایهها و کنترلهای نمایش تدریجی سر جایشاناند. منطق در یازده ماژول مستقل داخل `scripts/` نوشته میشود و هیچکدام نباید به `DOM` دسترسی داشته باشند؛ `main.js` تنها جایی است که به صفحه و `Canvas` وصل میشود.
+ **نکته:** نام فایلها و نام `export`ها را عوض نکنید. سیستم داوری ماژولها را از `window.studioApi` میگیرد و اگر ماژولی جا بیفتد، آن شیء ناقص میماند.
+ **نکته:** در هر فایل، اسکلت همهٔ `export`های لازم با بدنهٔ `TODO` آمده است. نام فایلها و نام `export`ها را تغییر ندهید.
+ **نکته:** برخلاف بقیهٔ سوالها، سیستم داوری اینجا ماژولها را مستقیم `import` نمیکند. صفحه را در مرورگر باز میکند و از طریق دو شیء `window.studioApi` و `window.studio` با کد شما کار میکند. پس اگر `main.js` این دو شیء را نسازد، **هیچ تستی پاس نمیشود.**
در `main.js` هم شیء `el` از قبل به تکتک عنصرهای صفحه اشاره میکند، دو مرحلهٔ رندر و رفتار نوار ابزار بهعنوان الگو نوشته شدهاند و جای بقیه با کامنت `TODO` و راهنما مشخص است. پس هیچوقت لازم نیست دنبال سلکتور بگردید یا نشانهگذاری بسازید؛ فقط منطق و رفتار را مینویسید.
</details>
# **جزئیات پیادهسازی**
منطق برنامه در یازده ماژول مستقل نوشته میشود که به `DOM` دسترسی ندارند. فایل `main.js` این ماژولها را به رابط کاربری و `Canvas` وصل میکند.
+ **نکته:** برخلاف بیشتر سوالها، توابع این سوال دادهٔ ورودی را مستقیم تغییر میدهند. توابع `grid.js` و `paint.js` و `select.js` و همچنین `moveLayer` و `setVisible` در `layers.js` روی همان ساختار ورودی کار میکنند و کپی تازه نمیسازند؛ مقدار بازگشتیشان فهرست سلولهای تغییرکرده است، نه شبکهٔ جدید. تنها استثنا `composite` است که شبکهٔ پایه را دستنخورده نگه میدارد و شبکهٔ تازهای برمیگرداند.
|  |
| :-: |
| نمایان شدن خانهبهخانهٔ طرح روی شبکه؛ همین جریان را در اجرای واقعی برنامه نشان میدهد |
<details class="green">
<summary>**ترتیب پیشنهادی برای پیادهسازی سوال**</summary>
1. `grid.js` مدل پایه است و بقیه ماژولها به آن نیاز دارند.
2. `paint.js` و `palette.js` مستقیم روی مدل کار میکنند.
3. `font.js` و `layers.js` میتوانند با هم پیش بروند.
4. `select.js` و `reveal.js` و `playback.js` هرکدام مستقلاند.
5. `templates.js` و `history.js` و `io.js` کوچکاند.
6. و آخر از همه به سراغ پیادهسازی `main.js` بروید.
</details>
<details class="blue">
<summary>**ساختار داده**</summary>
**شبکه** مستطیلی از سلولهاست. هر سلول یک شیء `{ color, glyph }` دارد و سلولِ نقاشینشده مقدار `EMPTY` میگیرد:
```js
export const EMPTY = null;
// cells holds rows x cols entries in row-major order
const grid = { rows: 24, cols: 40, cells: [] };
```
> شبکه مستطیلی از سلولهاست و هر سلول یک شیء با رنگ و نویسه است. مقدار `EMPTY` نشان میدهد سلول هنوز رنگ نشده و همهجا بهجای رنگ خالی از همین ثابت استفاده میشود، تا مقایسهها یکدست بماند.
>
> آرایهٔ `cells` دقیقاً `rows × cols` عضو دارد و بهصورت سطری پر میشود، یعنی اندیس هر سلول از شمارهٔ سطرش ضربدر تعداد ستونها بهعلاوهٔ شمارهٔ ستونش بهدست میآید. همین رابطه پایهٔ همهٔ توابع `grid.js` است.
**لایه** محتوایی است که روی شبکه قرار میگیرد:
```js
{ id: 1, row: 0, col: 0, width: 7, height: 5, visible: true,
cells: [ { row: 0, col: 1, color: "#fff" } ] }
```
> این مثال ساختار یک **لایه** را نمایش میدهد. هر لایه یک مستطیل روی شبکه اشغال میکند که با `row` و `col` و ابعادش مشخص میشود و `visible` میگوید در ترکیب نهایی دیده شود یا نه. لایهها روی شبکهٔ پایه سوار میشوند و ترتیبشان در پشته تعیین میکند کدام روی کدام بیفتد.
>
> مختصات داخل `cells` هر لایه نسبت به خودِ لایه است، نه نسبت به شبکه. برای گرفتن مختصات مطلق باید `row` و `col` لایه به آنها اضافه شود.
مختصات داخل `cells` **نسبت به خود لایه** است. `absoluteCells(layer)` آنها را با `row` و `col` لایه جمع میکند تا مختصات نهایی روی شبکه به دست بیاید. **انتخاب** یک مستطیل است:
```js
{ top: 1, left: 2, bottom: 3, right: 4 }
```
> ناحیهٔ انتخاب یک مستطیل روی شبکه است که با چهار مختصات مشخص میشود. هر چهار مرز شامل خودشاناند، یعنی سطر `top` و سطر `bottom` هر دو داخل انتخاب حساب میشوند.
>
> توابع `select.js` همیشه انتخاب را به همین شکل میگیرند و برمیگردانند، حتی وقتی کاربر از پایین به بالا انتخاب کرده باشد.
**سند** چیزی است که ذخیره و بازیابی میشود:
```js
{ grid, layers, palette, revealMode }
```
> این شکل **سند** است؛ همان چیزی که ذخیره و بازیابی میشود. شبکهٔ پایه، پشتهٔ لایهها، پالت رنگ و حالت نمایش تدریجی، چهار چیزیاند که با هم یک طرح کامل را میسازند.
>
> هنگام ذخیره همین شیء به `JSON` تبدیل میشود، پس نباید هیچ مقدار غیرقابلسریالسازی مثل تابع یا `undefined` داخلش بگذارید.
</details>
<details class="green">
<summary>**پیادهسازی فایل `grid.js`**</summary>
**شبکه** پایهایترین ساختار این سوال است و هر ماژول دیگری در نهایت روی همین کار میکند. سلولها در یک آرایهٔ **تخت** نگه داشته میشوند، نه آرایهٔ تودرتو؛ یعنی بهجای `cells[row][col]` یک آرایهٔ یکبعدی دارید که سطربهسطر پر شده است. برای همین اولین چیزی که باید بسازید تبدیل مختصات دوبعدی به اندیس تخت است و بقیهٔ توابع روی آن سوار میشوند.
**نکتهٔ دوم** که در کل این سوال با شما میماند: توابعی که شبکه را تغییر میدهند، آن را **در جای خود** عوض میکنند و کپی تازه نمیسازند. تنها استثناها `cloneGrid` و `resizeGrid` و `toJSON` هستند که صریحاً ساختار تازه میسازند. در قسمت زیر تعاریف و نحوه پیادهسازی هر کدام از توابع این ماژول بررسی میشوند:
- **تابع `createGrid(rows, cols)`:** شبکهٔ تازهای میسازد و شیئی با سه فیلد `rows` و `cols` و `cells` برمیگرداند. هر سلول شیئی به شکل `{ color, glyph }` است که در ابتدا رنگش `EMPTY` و نویسهاش رشتهٔ خالی است. ابعاد کمتر از یک پذیرفته نمیشوند و به `1` گرد میشوند، پس شبکه هیچوقت خالی از سلول نیست.
- **توابع `indexOf(grid, row, col)` و `inBounds(grid, row, col)`:** این دو پایهٔ همهچیزند. اولی مختصات دوبعدی را به اندیس داخل آرایهٔ `cells` تبدیل میکند و برای مختصات بیرون شبکه `-1` میدهد. دومی همان بررسی را بهصورت بولی جواب میدهد. با پیادهسازی درست این دو تابع، بیشتر توابع این ماژول ساده خواهند بود.
- **توابع `getCell(grid, row, col)` و `getColor(grid, row, col)`:** اولی خودِ شیء سلول را میدهد و برای مختصات بیرون شبکه `null`. دومی فقط رنگ همان سلول را میدهد؛ رنگ سلولِ نقاشینشده `EMPTY` است و برای مختصات بیرون شبکه هم همان `EMPTY` برمیگردد، نه خطا. این رفتار عمدی است: ابزارهای رسم مدام رنگ همسایهها را میپرسند و نباید مجبور باشند هر بار مرز شبکه را جدا بررسی کنند.
- **توابع `setColor(grid, row, col, color)` و `setGlyph(grid, row, col, glyph)`:** رنگ یا نویسهٔ یک سلول را **در همان شبکه** عوض میکنند و مقدار بولی برمیگردانند که میگوید تغییر انجام شد یا نه. برای مختصات بیرون شبکه هیچ کاری نمیکنند و `false` میدهند. رابط کاربری از همین مقدار بولی استفاده میکند تا بفهمد لازم است دوباره رسم کند یا نه.
- **تابع `clearGrid(grid)`:** همهٔ سلولهای همان شبکه را به حالت خالی برمیگرداند. ابعاد شبکه دستنخورده میماند و فقط محتوای سلولها پاک میشود.
- **توابع `paintedCount(grid)` و `cellCount(grid)`:** اولی تعداد سلولهای **رنگشده** و دومی تعداد **کل** سلولهای شبکه را برمیگرداند. نوار وضعیت برنامه از نسبت این دو استفاده میکند تا درصد پوشش طرح را نشان بدهد.
- **تابع `resizeGrid(grid, rows, cols)`:** شبکهای با ابعاد تازه میسازد و **شبکهٔ جدید را برمیگرداند**؛ شبکهٔ ورودی دستنخورده میماند. رفتار مهمش این است که محتوای قبلی را دور نمیریزد: هر سلولی که مختصاتش در هر دو شبکه معتبر باشد، رنگ و نویسهاش منتقل میشود. یعنی بزرگ کردن شبکه طرح موجود را نگه میدارد و کوچک کردن فقط بخش بیرونافتاده را میاندازد.
- **تابع `cloneGrid(grid)`:** کپی کاملاً مستقلی از شبکه میسازد. منظور از مستقل این است که سلولها هم کپی شوند، نه اینکه آرایهٔ تازه به همان شیءهای قبلی اشاره کند؛ وگرنه تغییر در کپی، اصل را هم خراب میکند. تاریخچهٔ بازگشت روی همین تابع بنا میشود.
- **توابع `toJSON(grid)` و `fromJSON(obj)`:** قرینهٔ هماند و پل میان شبکه و فایل ذخیرهشدهاند. اولی شبکه را به شیء سادهای تبدیل میکند که قابل تبدیل به `JSON` باشد و دومی از روی همان شیء، شبکه را بازمیسازد. تابع `fromJSON` باید در برابر دادهٔ ناقص مقاوم باشد: اگر شیء ورودی سلولهای کمتری داشته باشد یا فیلدی جا افتاده باشد، بهجای خطا باید مقدار پیشفرض بگذارد.
</details>
<details class="violet">
<summary>**پیادهسازی فایل `paint.js`**</summary>
**این ماژول شامل ابزارهای نقاشی است.** همان چیزهایی که در هر نرمافزار پیکسلآرت زیر دست کاربر است. همهٔ این توابع یک قرارداد مشترک دارند که باید از اول رعایتش کنید: شبکه را **در جای خود** تغییر میدهند و چیزی که برمیگردانند شبکهٔ جدید نیست، بلکه **فهرست سلولهایی است که واقعاً عوض شدند**، به شکل `[{ row, col }]`. دلیل این طراحی کارایی رسم است؛ رابط کاربری با دیدن این فهرست فقط همان چند سلول را دوباره روی `Canvas` میکشد، نه کل شبکه را.
از این قرارداد یک نکتهٔ مهم نتیجه میشود که سیستم داوری کوئرا روی آن **حساس** است: سلولی که رنگش عوض **نشده** نباید در خروجی بیاید. رنگزدن سلولی که از قبل همان رنگ را دارد و همچنین هر مختصات بیرون شبکه، باید فهرست خالی بدهد. در قسمت زیر تعاریف و نحوه پیادهسازی هر کدام از توابع این ماژول بررسی میشوند:
- **تابع `brush(grid, row, col, color)`:** سادهترین ابزار: یک سلول را رنگ میکند. اگر رنگ سلول واقعاً عوض شود، مختصاتش در فهرست خروجی میآید؛ در غیر این صورت فهرست خالی است.
- **تابع `eraser(grid, row, col)`:** قرینهٔ `brush` است و یک سلول را به حالت خالی برمیگرداند. همان قاعده اینجا هم برقرار است: پاک کردن سلولی که از قبل خالی بوده، تغییری نیست و فهرست خالی میدهد.
- **تابع `mirrorCol(grid, col)`:** شمارهٔ ستون قرینه را نسبت به محور عمودی وسط شبکه برمیگرداند. یعنی ستون اول به ستون آخر نگاشت میشود و ستون وسط در شبکهای با تعداد ستون فرد، قرینهٔ خودش است. این تابع شبکه را تغییر نمیدهد و فقط یک عدد برمیگرداند.
- **تابع `brushSymmetric(grid, row, col, color)`:** حالت قرینهای که تیفوسازها زیاد استفاده میکنند: همزمان سلول اصلی و قرینهٔ افقیاش را رنگ میکند تا طرح متقارن بماند. خروجیاش فهرست همهٔ سلولهایی است که عوض شدند؛ وقتی سلول روی محور تقارن باشد، سلول اصلی و قرینه یکیاند و طبیعتاً فقط یک مختصات در فهرست میآید.
- **تابع `bucket(grid, row, col, color)`:** همان ابزار سطل رنگ است: از سلولی که کلیک شده شروع میکند و ناحیهٔ پیوستهٔ همرنگ را با رنگ تازه پر میکند و تا جایی پیش میرود که به رنگ متفاوت یا مرز شبکه برسد. تعریف «همسایه» اینجا فقط چهار جهت اصلی است: بالا، پایین، چپ و راست. سلولهای قطری همسایه حساب نمیشوند. برای نمونه اگر در یک شبکهٔ ۳×۳ ستون وسط را رنگ کرده باشید و بعد سطل را روی سلولِ `(0, 0)` بزنید، فقط سه سلول ستون چپ پر میشوند، چون ستون رنگشدهٔ وسط دیوار است. اگر رنگ تازه با رنگ فعلی همان سلول یکی باشد، هیچ اتفاقی نمیافتد.
- **تابع `line(grid, r0, c0, r1, c1, color)`:** خط مستقیمی بین دو سلول میکشد. چون شبکه گسسته است، باید مشخص باشد خط دقیقاً از کدام سلولها میگذرد و همهٔ پیادهسازیها به یک جواب برسند؛ به همین دلیل از الگوریتم استاندارد `Bresenham` استفاده کنید. **هر دو سر خط** هم رنگ میشوند، یعنی سلول شروع و سلول پایان جزو خطاند.
- **تابع `rect(grid, r0, c0, r1, c1, color)`:** مستطیل توپر میکشد. دو گوشهٔ دادهشده **داخل** مستطیل حساب میشوند، پس مستطیلی که از سطر `0` تا `1` و ستون `0` تا `2` کشیده شود، شش سلول دارد. ترتیب گوشهها اهمیتی ندارد و باید برای هر ترتیبی نتیجهٔ یکسان بدهد.
```js
const g = createGrid(5, 5);
brush(g, 2, 2, "#fff") // [{ row: 2, col: 2 }]
mirrorCol(createGrid(3, 7), 0) // 6
mirrorCol(createGrid(3, 7), 3) // 3
line(g, 0, 0, 4, 4, "#fff").length // 5 (diagonal)
rect(g, 0, 0, 1, 2, "#fff").length // 6 (2 rows x 3 cols)
bucket(createGrid(4, 4), 0, 0, "#fff").length // 16
bucket(createGrid(3, 3), 0, 0, null).length // 0 (same color)
```
> مثالهای بالا رفتار مورد انتظار ابزارهای رسم را نشان میدهند. توابع این ماژول شبکه را **در جای خود** تغییر میدهند و کپی تازه نمیسازند؛ چیزی که برمیگردانند فهرست سلولهای تغییرکرده است، نه شبکهٔ جدید. همین فهرست به رندر میگوید فقط کدام سلولها را دوباره بکشد.
>
> به حالتهای مرزی دقت کنید: رنگزدن سلولی که از قبل همان رنگ را دارد نباید چیزی در خروجی بگذارد و مختصات بیرون شبکه هم باید بیاثر باشد. ابزار سطل تا مرز رنگ متفاوت پیش میرود و فقط چهار جهت اصلی را دنبال میکند.
</details>
<details class="orange">
<summary>**پیادهسازی فایل `palette.js`**</summary>
**پالت** همان نوار رنگی است که کاربر از میانش رنگ فعال را انتخاب میکند و میتواند رنگ سفارشی هم به آن اضافه کند. کل حالتش دو چیز است: فهرست رنگها و اینکه کدامشان الان فعال است.
بخش پرنکتهٔ این ماژول **اعتبارسنجی رنگ** است. کاربر میتواند هر متنی در جعبهٔ رنگ سفارشی بنویسد، پس باید ورودی را پیش از اضافه کردن هم **بررسی** و هم **یکدست** کنید. در قسمت زیر تعاریف و نحوه پیادهسازی هر کدام از توابع این ماژول بررسی میشوند:
- **تابع `createPalette(swatches = DEFAULT_SWATCHES)`:** پالت تازهای میسازد و برمیگرداند. اگر فهرست رنگ ندهید، از `DEFAULT_SWATCHES` استفاده میشود که خودتان تعریفش میکنید و باید دستکم چهار رنگ داشته باشد. رنگ فعال در ابتدا اولین رنگ فهرست است.
- **تابع `activeColor(palette)`:** رنگ انتخابشدهٔ فعلی را برمیگرداند. ابزارهای نقاشی رنگشان را از همینجا میگیرند.
- **تابع `selectIndex(palette, index)`:** رنگ فعال را با شمارهٔ جایگاهش عوض میکند. اندیس بیرون بازه خطا نمیدهد و به نزدیکترین جایگاه مجاز محدود میشود، پس عدد منفی به رنگ اول و عدد بزرگتر از طول پالت به رنگ آخر میرسد.
- **تابع `normalizeHex(value)`:** رنگ ورودی را به یک شکل استاندارد درمیآورد: **ششرقمی و با حروف کوچک**. قالب کوتاه سهرقمی هم پذیرفته میشود و به شکل کامل باز میشود. فقط دو قالب `#RGB` و `#RRGGBB` معتبرند؛ نام رنگهای `CSS` و قالب `rgb()` نامعتبرند و برای هر ورودی نامعتبر باید `null` برگردد، نه خطا و نه رشتهٔ خالی. این تابع دروازهٔ ورود هر رنگ سفارشی به پالت است.
- **تابع `addCustom(palette, value)`:** رنگ تازهای به پالت اضافه میکند و همان را رنگ فعال میکند و رنگ استانداردشده را برمیگرداند. سه رفتار دارد که سیستم داوری میسنجد: ورودی نامعتبر اضافه نمیشود و `null` برمیگردد؛ رنگی که از قبل در پالت هست دوباره اضافه نمیشود؛ و مقایسهٔ تکراری بودن روی شکل **استانداردشدهٔ** رنگ انجام میشود، پس `#ABCDEF` و `#abcdef` یک رنگاند.
- **تابع `count(palette)`:** تعداد رنگهای پالت را برمیگرداند.
```js
normalizeHex("#ABCDEF") // "#abcdef"
normalizeHex("#0f8") // "#00ff88"
normalizeHex("not-a-color") // null
const p = createPalette(["#aaa"]);
addCustom(p, "#123456") // "#123456"
addCustom(p, "xyz") // null
```
> بلوک بالا نشان میدهد رنگ ورودی چطور یکدست میشود. خروجی همیشه ششرقمی و با حروف کوچک است، پس `#ABCDEF` به `#abcdef` تبدیل میشود و قالب کوتاه `#0f8` به شکل کامل باز میشود.
>
> فقط قالبهای `#RGB` و `#RRGGBB` معتبرند؛ نام رنگ و `rgb()` پذیرفته نمیشوند و برای ورودی نامعتبر باید `null` برگردد. این تابع پیش از افزودن هر رنگ سفارشی به پالت صدا زده میشود.
</details>
<details class="teal">
<summary>**پیادهسازی فایل `font.js` و `layers.js`**</summary>
هر نویسه با یک ماتریس **۵ ردیف در ۳ ستون** از سلولهای روشن و خاموش نوشته میشود:
```js
export const GLYPH_W = 3;
export const GLYPH_H = 5;
```
> این ثابتها ابعاد ماتریس هر نویسهاند. هر حرف روی شبکهای با همین عرض و ارتفاع از خانههای روشن و خاموش نوشته میشود و متن، از کنار هم چیدن همین ماتریسها ساخته میشود.
>
> نویسههای ناشناخته باید خالی رندر شوند و برنامه نباید خطا بدهد. فاصلهٔ میان نویسهها در ارزیابی سنجیده نمیشود، ولی عرض خروجی باید با طول متن بیشتر شود.
روی شبکه فونتی در کار نیست؛ حرفها را باید خودتان با روشن و خاموش کردن خانهها بکشید، دقیقاً همان کاری که تماشاگران روی سکو با بالا بردن مقواهای رنگی میکنند. در قسمت زیر تعاریف و نحوه پیادهسازی هر کدام از توابع این ماژول بررسی میشوند:
- **تابع `glyph(ch)`:** ماتریس یک نویسه را برمیگرداند: جدولی با ابعاد `GLYPH_H` در `GLYPH_W` که مشخص میکند کدام خانهها روشناند. حروف بزرگ انگلیسی و ارقام را باید پوشش بدهید. نویسهای که در فهرست شما نیست نباید خطا بدهد؛ برایش ماتریس کاملاً خالی برمیگردد تا برنامه با یک نویسهٔ ناشناخته از کار نیفتد.
- **تابع `renderText(text)`:** کل یک متن را رندر میکند: نویسهها را از چپ به راست با فاصله کنار هم میچیند و نتیجه را به شکل شیئی با فهرست سلولهای روشن و `width` و `height` برمیگرداند. پیش از رسم، حروف به حرف بزرگ تبدیل میشوند، پس `a` و `A` خروجی یکسان میدهند. مقدار `height` همیشه برابر ارتفاع یک نویسه است و `width` با طولانیتر شدن متن بیشتر میشود.
لایه چیزی است که **روی** شبکهٔ پایه مینشیند بدون اینکه آن را خراب کند؛ میشود جابهجا، پنهان یا حذفش کرد و شبکهٔ زیرش دستنخورده میماند. هر لایه مختصات سلولهایش را **نسبی** نگه میدارد، یعنی نسبت به گوشهٔ بالا-چپ خودش و موقعیت خودش را در `row` و `col` دارد. همین باعث میشود جابهجا کردن لایه فقط تغییر دو عدد باشد.
|  |
| :-: |
| طرح پس از افزودن لایههای متن و شکل |
- **توابع `textLayer(text, row, col, color)` و `shapeLayer(shape, row, col, w, h, color)`:** این دو سازندهٔ لایهاند و هر کدام یک شیء لایه برمیگردانند با شناسهٔ یکتا، نوع، موقعیت، رنگ، فهرست سلولهای نسبی، ابعاد و فیلد `visible` که در ابتدا `true` است. اولی از دل `renderText` میآید و لایهٔ متنی میسازد. دومی لایهٔ شکل میسازد و مقدار `shape` آن یا `"rect"` است یعنی مستطیل توپر، یا `"border"` یعنی فقط قاب بیرونی. برای نمونه یک `border` با ابعاد ۴×۴ دوازده سلول دارد، چون چهار سلول میانی خالی میمانند.
- **توابع `createStack()` و `addLayer(stack, layer)`:** اولی پشتهٔ خالی لایهها را میسازد. دومی لایه را به پشته اضافه میکند و **خود لایه** را برمیگرداند، نه پشته را؛ اینطور میشود بلافاصله شناسهٔ لایهٔ تازه را گرفت و از آن استفاده کرد.
- **تابع `_resetIds()`:** شمارندهٔ شناسهٔ لایهها را از نو شروع میکند تا شناسهها تکرارپذیر بمانند. در رابط کاربری به کار نمیآید.
- **توابع `removeLayer(stack, id)` و `findLayer(stack, id)`:** اولی لایه را از همان پشته حذف میکند و مقدار بولی میدهد که آیا حذف انجام شد. دومی لایه را با شناسهاش پیدا میکند و برای شناسهٔ ناشناخته `null` میدهد. هر دو با شناسهٔ ناشناخته خطا نمیدهند.
- **توابع `moveLayer(stack, id, row, col)` و `setVisible(stack, id, visible)`:** لایه را در همان پشته جابهجا یا پنهان و آشکار میکنند و مقدار بولی موفقیت برمیگردانند. چون مختصات سلولها نسبی است، جابهجا کردن لایه فقط `row` و `col` خودش را عوض میکند.
- **توابع `zIndexOf(stack, id)` و `raise(stack, id)` و `lower(stack, id)`:** این سه ترتیب رویهمافتادن لایهها را مدیریت میکنند. `zIndexOf` جایگاه لایه در پشته را میدهد؛ جایگاه `0` یعنی پایینترین لایه و جایگاه بزرگتر یعنی بالاتر و نزدیکتر به بیننده. دو تابع دیگر لایه را یک پله بالا یا پایین میبرند و مقدار بولی برمیگردانند. اگر لایه از قبل در بالاترین یا پایینترین جایگاه باشد، هیچ کاری انجام نمیشود و `false` برمیگردد؛ در صورت قرار داشتن لایه در ابتدا یا انتهای پشته، ترتیب لایهها از ابتدا تکرار نمیشود.
- **تابع `absoluteCells(layer)`:** مختصات نسبی سلولهای لایه را با موقعیت خود لایه جمع میکند و مختصات **مطلق** آنها روی شبکه را برمیگرداند. این تابع پل میان مختصات محلی لایه و مختصات شبکه است.
- **تابع `composite(baseGrid, stack)`:** تصویر نهایی را میسازد: شبکهٔ پایه بهعلاوهٔ همهٔ لایهها. این تنها تابع ماژول است که شبکهٔ ورودی را دست نمیزند و **شبکهٔ تازه** برمیگرداند. چهار قاعده دارد:
+ شبکهٔ پایه دستنخورده میماند و خروجی یک شبکهٔ مستقل تازه است.
+ لایهها از پایین به بالا کشیده میشوند، پس لایهٔ بالاتر رنگ لایهٔ پایینتر را میپوشاند.
+ لایهای که `visible` آن `false` است اصلاً کشیده نمیشود.
+ سلولهایی از شبکهٔ پایه که هیچ لایهای رویشان نیفتاده، رنگ خودشان را نگه میدارند.
+ سلولهای لایه که بیرون مرز شبکه میافتند نادیده گرفته میشوند و خطا نمیدهند.
</details>
<details class="purple">
<summary>**پیادهسازی فایل `select.js`**</summary>
**انتخاب همان کادر نقطهچینی است** که کاربر دور بخشی از طرح میکشد تا آن تکه را یکجا رنگ یا جابهجا کند. خود انتخاب فقط یک مستطیل است و به شکل `{ top, left, bottom, right }` نگه داشته میشود؛ سلولهای داخلش هرجا لازم شد از روی همین چهار عدد حساب میشوند.
یک قاعده در کل این ماژول برقرار است: **هر چهار مرز داخل انتخاب حساب میشوند.** یعنی انتخابی از سطر `0` تا `1` دو سطر دارد، نه یک سطر. در قسمت زیر تعاریف و نحوه پیادهسازی هر کدام از توابع این ماژول بررسی میشوند:
- **تابع `marquee(r0, c0, r1, c1)`:** از دو گوشهای که کاربر با کشیدن ماوس مشخص کرده یک مستطیل مرتب میسازد. کاربر میتواند کادر را از هر جهتی بکشد، پس ورودی میتواند جابهجا باشد؛ خروجی همیشه مستطیلی است که `top` از `bottom` کوچکتر و `left` از `right` کوچکتر است.
- **توابع `cellsIn(sel)` و `contains(sel, row, col)` و `size(sel)`:** این سه تابع اطلاعات هندسی انتخاب را برمیگردانند. اولی فهرست همهٔ سلولهای داخل انتخاب را میدهد، دومی میگوید یک سلول مشخص داخل انتخاب هست یا نه و مقدار بولی برمیگرداند و سومی ابعاد انتخاب را به شکل `{ width, height }` میدهد. چون مرزها داخل حساب میشوند، انتخابی از ستون `0` تا `3` عرض `4` دارد نه `3`.
- **تابع `snapshot(grid, sel)`:** از محتوای داخل انتخاب یک کپی میگیرد. مختصات این کپی **نسبی** است، یعنی نسبت به گوشهٔ بالا-چپ خود انتخاب و به شکل `[{ dr, dc, color }]` برمیگردد. همین نسبی بودن است که اجازه میدهد بعداً محتوا را در جای دیگری بنشانید بدون اینکه چیدمان داخلیاش به هم بخورد.
- **تابع `moveSelection(grid, sel, dRow, dCol)`:** محتوای انتخاب را بهاندازهٔ دادهشده جابهجا میکند و **مستطیل انتخاب جدید** را برمیگرداند، نه شبکه را. چند نکته وجود دارد که سیستم داوری کوئرا همین موارد را بررسی میکند:
+ از آنجایی که این عمل جابهجایی است و نه کپی کردن، جای قبلی خالی میشود.
+ چیدمان نسبی سلولها حفظ میشود؛ اگر دو سلول یک واحد فاصله داشتند، بعد از جابهجایی هم همان فاصله را دارند.
+ بخشی از انتخاب که بیرون مرز شبکه بیفتد از بین میرود و بقیه سر جای تازهشان مینشینند. اگر کل انتخاب بیرون برود، محتوایش کاملاً از دست میرود.
توجه کنید که مبدأ و مقصد میتوانند همپوشانی داشته باشند؛ اگر جابهجایی را سلولبهسلول و بدون کپی گرفتن انجام بدهید، داده خراب میشود.
- **تابع `fillSelection(grid, sel, color)`:** همهٔ سلولهای داخل انتخاب را با یک رنگ پر میکند و مثل ابزارهای `paint.js` فهرست سلولهای تغییرکرده را برمیگرداند.
```js
size(marquee(0, 0, 2, 3)) // { width: 4, height: 3 }
moveSelection(g, marquee(0, 0, 1, 1), 2, 1)
// { top: 2, left: 1, bottom: 3, right: 2 }
```
> بلوک بالا رفتار ناحیهٔ انتخاب را نشان میدهد. تابع `marquee` از دو نقطه یک مستطیل میسازد و چون هر دو مرز شامل خودشاناند، بازهٔ `0` تا `3` عرض `4` میدهد نه `3`.
>
> تابع `moveSelection` محتوای انتخاب را جابهجا میکند و جای قبلی را خالی میگذارد. اگر بخشی از انتخاب بیرون شبکه برود همان بخش حذف میشود و پیش از پاککردن مبدأ باید یک کپی از محتوا بگیرید تا جابهجاییهای همپوشان داده را خراب نکنند.
</details>
<details class="yellow">
<summary>**پیادهسازی فایل `reveal.js` و `playback.js`**</summary>
**«نمایش تدریجی»** همان افکتی است که تیفو با آن روی سکو باز میشود: سلولها یکباره ظاهر نمیشوند، بلکه به ترتیبی مشخص یکییکی روشن میشوند. این قابلیت از دو بخش تشکیل شده که در دو فایل جداگانه پیادهسازی میشوند. `reveal.js` میگوید سلولها **به چه ترتیبی** روشن شوند و `playback.js` میگوید نوار پیشرفت **چطور جلو برود**.
ترتیبهای نمایش هر کدام یک آرایه از مختصات سلولها برمیگردانند، از اولین سلولی که روشن میشود تا آخرین:
- **تابع `orderRows(grid)`:** سلولها را سطربهسطر میچیند و از سلولِ `(0, 0)` شروع میکند.
- **تابع `orderCols(grid)`:** ستونبهستون میچیند، پس در یک شبکهٔ ۲×۲ عضو دوم سلولِ `(1, 0)` است نه `(0, 1)`.
- **تابع `orderWave(grid)`:** موج قطری میسازد: سلولها بر اساس فاصلهشان از گوشهٔ بالا-چپ گروهبندی میشوند، پس ترتیب از `(0, 0)` شروع و به گوشهٔ مقابل ختم میشود.
- **تابع `orderSpiral(grid)`:** ترتیب مارپیچی از مرکز به بیرون میدهد؛ در شبکهٔ ۵×۵ عضو اول سلولِ `(2, 2)` است. وقتی تعداد سطر یا ستون زوج باشد شبکه مرکز دقیق ندارد؛ در آن حالت سلول **بالاتر و چپتر** را مرکز بگیرید تا خروجی یکتا بماند.
- **تابع `modes()`:** فهرست نام حالتها را برمیگرداند و باید دقیقاً این چهار نام باشد: `rows` و `cols` و `wave` و `spiral`.
- **تابع `buildReveal(grid, mode = "rows")`:** ترتیب انتخابشده را میسازد و به هر سلول یک `threshold` بین `0` و `1` میچسباند؛ یعنی «این سلول از چه لحظهای از نمایش به بعد روشن است». اولین سلول `threshold` برابر `0` و آخرین سلول `1` میگیرد و بقیه بهطور یکنواخت میان این دو پخش میشوند. اگر `mode` داده نشود، ترتیب سطری استفاده میشود.
- **توابع `revealedAt(revealList, progress)` و `revealedCount(revealList, progress)`:** میگویند در یک لحظهٔ مشخص از نمایش، کدام سلولها روشناند؛ اولی خود سلولها را میدهد و دومی فقط تعدادشان را. قاعده این است که سلولی روشن است که `threshold` آن از `progress` بیشتر **نباشد**. یک نتیجهٔ مهم از این قاعده بیرون میآید که سیستم داوری میسنجد: چون `threshold` اولین سلول برابر `0` است، در `progress` برابر صفر دقیقاً **یک** سلول روشن است، نه هیچ سلولی؛ و در `progress` برابر یک، همهٔ سلولها روشناند.
|  |
| :-: |
| شبکه در میانهٔ نمایش تدریجی طرح |
در ادامه، کنترل پخش را پیادهسازی میکنیم. حالت پخش شیئی است که پیشرفت فعلی، وضعیت پخش و سرعت را نگه میدارد:
- **تابع `createPlayback()`:** وضعیت اولیه را میسازد: `progress` برابر `0` و `playing` برابر `false`.
- **تابع `seek(pb, progress)`:** پیشرفت را روی مقدار دادهشده میگذارد و آن را بین `0` و `1` مهار میکند.
- **توابع `play(pb)` و `pause(pb)` و `toggle(pb)`:** پخش را روشن، خاموش یا برعکس میکنند. `play` یک رفتار ویژه دارد: اگر نمایش از قبل تا انتها رفته باشد، بهجای اینکه در همان انتها گیر کند، از ابتدا شروع میکند.
- **تابع `setSpeed(pb, speed)`:** سرعت پخش را میگذارد و آن را داخل بازهٔ مجاز نگه میدارد؛ بیشینهٔ مجاز `4` است.
- **تابع `tick(pb, dt)`:** پیشرفت را بهاندازهٔ زمان سپریشده جلو میبرد و در هر فریم صدا زده میشود. در حالت توقف هیچ کاری نمیکند. وقتی پیشرفت به `1` میرسد همانجا میایستد و `playing` خودبهخود `false` میشود، یعنی از عدد یک عبور نمیکند.
- **تابع `frameIndex(pb, frames)`:** پیشرفت را به شمارهٔ فریم تبدیل میکند. خروجی همیشه عددی صحیح در بازهٔ `0` تا `frames - 1` است؛ برای پیشرفت `0.5` و یازده فریم نتیجه `5` است و اگر `frames` صفر باشد، مقدار `0` برمیگردد.
- **تابع `atEnd(pb)`:** میگوید نمایش به انتها رسیده است یا نه و مقدار بولی برمیگرداند.
</details>
<details class="olive">
<summary>**پیادهسازی فایل `templates.js` و `history.js` و `io.js`**</summary>
قالبها طرحهای آمادهایاند که کاربر با یک کلیک روی شبکه مینشاند تا از صفر شروع نکند. سه تابع اول روی شبکهای که به آنها میدهید کار میکنند و **همان شبکه** را برمیگردانند. در قسمت زیر تعاریف و نحوه پیادهسازی هر کدام از توابع این ماژول بررسی میشوند:
- **تابع `tricolor(grid, colors)`:** پرچم سهنواره میسازد: سه نوار **افقی** همارتفاع با سه رنگ دادهشده. تعداد سطرها همیشه بر سه بخشپذیر نیست، پس باید تکلیف سطرهای اضافه روشن باشد: سطرهای اضافه به نوارهای **بالایی** میرسند.
- **تابع `verticalBands(grid, colors)`:** همان کار را با سه نوار **عمودی** همعرض انجام میدهد و ستونهای اضافه به نوارهای سمت **چپ** میرسند.
- **تابع `word(grid, text, fg, bg)`:** کل شبکه را با رنگ پسزمینه پر میکند و بعد متن را وسطچین با رنگ پیشزمینه رویش مینویسد. وقتی فضای باقیماندهٔ دو طرف برابر نمیشود، متن را به سمت **بالا و چپ** بچسبانید تا خروجی یکتا و قابلارزیابی بماند.
- **تابع `applyPreset(name, rows, cols)`:** با نام یک قالب و ابعاد خواستهشده، شبکهٔ **تازهای** میسازد و قالب را رویش اعمال میکند. برای نام ناشناخته `null` برمیگرداند، نه شبکهٔ خالی. یک شرط مهم دارد: بعد از اعمال هر قالب، کل شبکه باید رنگشده باشد، یعنی `paintedCount` با `cellCount` برابر شود و هیچ سلول خالی نماند.
- **تابع `presetNames()`:** فهرست نام قالبهای موجود را برمیگرداند و باید دقیقاً شامل این سه نام باشد: `flag-tricolor` و `word-goal` و `word-2026`.
**تاریخچه** امکان پیادهسازی عملیات *Undo* و *Redo* را فراهم میکند. مدلش یک فهرست از وضعیتها است بهعلاوهٔ یک نشانگر که میگوید الان روی کدام وضعیت ایستادهاید؛ بازگشت یعنی نشانگر یک قدم عقب برود، نه اینکه چیزی از فهرست حذف شود.
+ `createHistory(initial)` تاریخچهٔ تازهای با یک وضعیت اولیه میسازد و برمیگرداند.
+ `push(history, state)` وضعیت تازهای را ثبت میکند.
+ `undo(history)` و `redo(history)` یک قدم عقب و جلو میروند و وضعیت جدید را برمیگردانند. اگر قدمی برای رفتن نمانده باشد، `null` میدهند.
+ `canUndo(history)` و `canRedo(history)` میگویند قدمی برای عقب یا جلو رفتن مانده است یا نه و مقدار **بولی** برمیگردانند.
+ `current(history)` وضعیت فعلی را برمیگرداند.
+ `depth(history)` تعداد کل وضعیتهای ثبتشده **از جمله وضعیت اولیه** را میدهد؛ یعنی بلافاصله بعد از `createHistory` برابر `1` است.
+ `push` بعد از یک `undo`، شاخهٔ `redo` را پاک میکند.
+ **نکته:** وضعیتهای ذخیرهشده باید کپی مستقل باشند. اگر شیئی را ثبت کنید و بعد همان شیء را تغییر بدهید، مقدار داخل تاریخچه نباید عوض شود؛ وگرنه بازگشت به وضعیت قبلی همان وضعیت فعلی را برمیگرداند و دکمهٔ `undo` عملاً بیاثر میشود.
ماژول ورودی و خروجی، طرح را به فایل تبدیل میکند و برعکس. سندی که ذخیره میشود شامل شبکه، لایهها و پالت است.
```js
export const SCHEMA_VERSION = 1;
```
> شمارهٔ نسخهٔ سند در فایل ذخیرهشده میآید تا اگر بعداً شکل داده عوض شد، بشود نسخهٔ قدیمی را تشخیص داد.
>
> هنگام بازیابی، سندی که این کلید را نداشته باشد یا ساختارش ناقص باشد نامعتبر است و برنامه باید با وضعیت اولیه بالا بیاید، نه اینکه خطا بدهد.
- **تابع `serialize(doc)`:** سند را به متن `JSON` تبدیل میکند و فیلد `version` را با مقدار `SCHEMA_VERSION` داخلش میگذارد. خروجی یک **رشته** است، نه شیء.
- **تابع `deserialize(text)`:** قرینهٔ تابع قبلی است: متن را به سند برمیگرداند. برای متنی که `JSON` معتبر نیست یا ساختار موردانتظار را ندارد، بهجای پرتاب خطا مقدار `null` برمیگرداند. این رفتار ضروری است، چون کاربر میتواند هر فایلی را روی برنامه بیندازد و برنامه نباید با یک فایل خراب از کار بیفتد.
- **تابع `isValidDocument(text)`:** بررسی میکند متن دادهشده هم `JSON` معتبر باشد و هم هر سه کلید `grid` و `layers` و `palette` را داشته باشد. نبودن هر کدام از این سه، سند را نامعتبر میکند. خروجی بولی است.
- **تابع `toBlob(doc)`:** همان متن `JSON` را داخل یک `Blob` با نوع `application/json` میگذارد تا دکمهٔ دانلود بتواند از رویش فایل بسازد.
- **تابع `exportPNG(canvas)`:** از `Canvas` یک خروجی تصویری به شکل `data:image/png` میسازد و برمیگرداند. برای ورودی نامعتبر مقدار `null` میدهد.
</details>
<details class="pink">
<summary>**رابط کاربری و فایل `main.js`**</summary>
فایل `main.js` باید دو شیء در دسترس بگذارد:
+ `window.studioApi` که ماژولها را با نام خودشان بیرون میدهد: `grid`، `paint`، `palette`، `layers`، `select`، `reveal`، `playback`، `templates`، `history`، `io`.
+ `window.studio` که وضعیت برنامه و چند متد کمکی دارد.
+ **قرارداد سیستم داوری:** این دو شیء فقط برای داوری لازماند؛ کاربر نهایی با آنها کاری ندارد.
شیء `window.studio` باید `model` داشته باشد با این فیلدها:
+ `grid` شبکهٔ رنگها
+ `stack` پشتهٔ لایهها، که `stack.layers` آرایهٔ لایههاست
+ `palette` که `palette.swatches` آرایهٔ رنگها و `palette.activeIndex` اندیس رنگ فعال است
+ `tool` و `symmetry` و `revealMode` و `revealList` و `playback`
|  |
| :-: |
| چیدمان کامل استودیو با پالت و پنل لایهها |
و این متدها را داشته باشد:
+ `paintCell(row, col, opts?)` که برای ابزار `line` با گزینهٔ `{ from: { row, col } }` نقطهٔ شروع خط را میگیرد
+ `eventToCell({ clientX, clientY })` مختصات صفحه را به سلولِ شبکه تبدیل میکند؛ گوشهٔ بالا-چپ `Canvas` باید به سلولِ `(0, 0)` نگاشت شود
+ `setTool(name)` ابزار فعال را عوض میکند، `toggleSymmetry()` حالت قرینه را روشن و خاموش میکند و `setColorIndex(i)` رنگ فعال پالت را انتخاب میکند.
+ `activeColor()` رنگ فعال را برمیگرداند و `addCustomColor(hex)` رنگ تازهای به پالت اضافه میکند.
+ `addTextLayer(text)` لایهٔ متنی تازهای میسازد و `compositeGrid()` شبکهٔ نهایی (پایه بهعلاوهٔ لایهها) را برمیگرداند.
+ `applyTemplate(name)` یک قالب آماده را روی شبکه مینشاند و `clearGrid()` کل شبکه را خالی میکند.
+ `undo()` و `redo()` یک قدم عقب و جلو میروند و `canUndo()` میگوید قدمی برای بازگشت مانده است یا نه.
+ `setRevealMode(mode)` حالت نمایش تدریجی را عوض میکند و `seekReveal(progress)` پیشرفت را میگذارد.
+ `revealProgress()` پیشرفت فعلی را بهصورت عددی بین `0` و `1` و `revealedCount()` تعداد سلولهای آشکارشده را برمیگردانند.
+ `exportJSON()` و `importJSON(text)` که برای متن نامعتبر `false` برمیگرداند
شبکهٔ اولیه باید دستکم ۱۰۱ سلول داشته باشد؛ پیشنهاد ما ۲۴ سطر در ۴۰ ستون است.
**تمام عنصرهای زیر از قبل در `index.html` هستند** و در `main.js` هم شیء `el` به همهشان اشاره میکند. کار شما ساختن آنها نیست؛ کار شما **بهروز نگهداشتن وضعیتشان از روی مدل** و **پر کردن ظرفهای خالی** است.
|  |
| :-: |
| نواحی اصلی صفحه و نام هرکدام |
**سه ظرف خالی که باید پرشان کنید:**
+ `palette` برای هر رنگ یک `swatch-<index>` میگیرد که `role="option"` و `aria-label` دارد؛ رنگ فعال `aria-selected="true"` و بقیه `"false"`.
+ `layer-list` برای هر لایه یک `layer-item-<id>` میگیرد و لایهٔ فعال `aria-current="true"` میشود. کنار هر لایه سه دکمهٔ `layer-remove-<id>` و `layer-raise-<id>` و `layer-visible-<id>` قرار میگیرد.
+ `template-buttons` برای هر قالب یک دکمهٔ `template-<name>` میگیرد و متن هر دکمه شامل نام همان قالب است.
**وضعیتهایی که باید با مدل همگام بمانند:**
+ پنج ابزار `tool-brush` و `tool-eraser` و `tool-bucket` و `tool-line` و `tool-select` از قبل `role="radio"` دارند؛ شما فقط `aria-checked` را جابهجا میکنید تا در هر لحظه دقیقاً یکی انتخابشده باشد. ترتیب `Tab` هم باید فقط روی ابزار انتخابشده بیفتد (`tabindex="0"` و بقیه `-1`) و کلیدهای جهتدار انتخاب را جابهجا کنند.
+ دکمهٔ `toggle-symmetry` حالت قرینه را عوض میکند و `aria-pressed` میگیرد.
+ دکمههای `undo` و `redo` باید بر اساس وضعیت تاریخچه فعال و غیرفعال شوند.
+ چهار گزینهٔ `reveal-rows` و `reveal-cols` و `reveal-wave` و `reveal-spiral` هم `aria-checked` خودشان را از حالت نمایش فعلی میگیرند.
+ دکمهٔ `play-reveal` مقدار `aria-pressed` میگیرد و اسلایدر `reveal-scrubber` از نوع `range` با `min="0"` و `max="100"` است، پس مقدار `50` یعنی پیشرفت `0.5`. متن `reveal-progress` درصد را با عدد نشان میدهد و در پیشرفت کامل باید `100` داخلش باشد.
**کلید میانبر:** کلید `b` در سطح `document` ابزار را به قلم برمیگرداند، ولی فقط وقتی تمرکز روی یک فیلد متنی یا عنصر قابل ویرایش نباشد؛ وگرنه تایپ حرف `b` داخل ورودی رنگ سفارشی، ابزار را عوض میکند.
**رنگ سفارشی:** ورودی `custom-color-input` یک **ورودی متنی** است، نه `type="color"`، چون سیستم داوری رشتهٔ `hex` را داخلش تایپ میکند. دکمهٔ `custom-color-add` رنگ را به پالت اضافه میکند.
**خروجی:** متن ناحیهٔ `io-status` بعد از هر کنش باید کلمهٔ کلیدی مربوطه را داشته باشد: بعد از خروجی `JSON` کلمهٔ `Saved`، بعد از خروجی تصویر کلمهٔ `PNG` و وقتی رنگ سفارشی نامعتبر است کلمهٔ `Invalid`.
**ذخیرهسازی:** وضعیت باید در `localStorage` زیر کلید دقیق `tifo-designer` ذخیره شود و بعد از باز شدن دوبارهٔ صفحه برگردد. اگر مقدار ذخیرهشده وجود نداشت، `JSON` معتبر نبود یا ساختارش با سند سازگار نبود، برنامه باید با وضعیت اولیه بالا بیاید و خطا پرتاب نکند.
</details>
# **آنچه سیستم داوری بررسی میکنند**
**سیستم داوری** ماژولها را از `window.studioApi` میگیرد و مستقیم صدا میزند و برای رابط کاربری با `window.studio` کار میکند.
+ **نکته:** سیستم داوری رویدادهای واقعی موس را روی `Canvas` شبیهسازی نمیکند، ولی متد `eventToCell({ clientX, clientY })` را مستقیم صدا میزند و انتظار دارد گوشهٔ بالا-چپ `Canvas` به سلولِ `(0, 0)` نگاشت شود. جز این، در نحوهٔ پیادهسازی تعامل با `Canvas` آزادید.
**رنگ، فونت و اندازهٔ پیکسلی بررسی نمیشوند. آنچه بررسی میشود به شکل زیر است:**
+ مقدار بازگشتی هر تابع برای ورودیهای مشخص، دقیقاً مطابق مثالهای بالا
+ تغییر کردن شبکه در توابع `grid` و `paint` و `select` و تغییر **نکردن** شبکهٔ پایه در `composite`
+ مستقل بودن کپیها در `cloneGrid` و `createHistory`
+ وجود عناصر با `data-testid`های گفتهشده و ویژگیهای `role="radiogroup"`، `aria-checked`، `aria-pressed`، `aria-selected`، `aria-current`، `aria-label` و `aria-live`
+ وضعیت `disabled` دکمهٔ `undo` در شروع
+ فیلدهای `window.studio.model` بعد از هر تغییر
+ ذخیره و بازیابی وضعیت در `localStorage`
آنچه بررسی **نمیشود** و میتوانید آزادانه و به دلخواه خودتان پیادهسازی کنید:
+ شکل دقیق ماتریس هر نویسه در `font.js`؛ فقط ارتفاع پنج ردیف، بزرگ شدن عرض با طول متن، خالی بودن نویسهٔ ناشناخته و یکسان بودن نتیجهٔ حروف کوچک و بزرگ سنجیده میشود
+ فاصلهٔ دقیق بین نویسهها
+ رنگهای `DEFAULT_SWATCHES`؛ فقط تعدادشان مهم است
+ ترتیب دقیق سلولها در `orderWave`؛ فقط سلولِ اول و آخر سنجیده میشود
+ طراحی بصری صفحه و ساختار `CSS`
# **آنچه باید آپلود کنید**
- **توجه:** فایل زیپ ارسالی شما باید همان ساختار پروژهٔ اولیه را داشته باشد:
```plaintext
├── scripts/
│ ├── font.js
│ ├── grid.js
│ ├── history.js
│ ├── io.js
│ ├── layers.js
│ ├── main.js
│ ├── paint.js
│ ├── palette.js
│ ├── playback.js
│ ├── reveal.js
│ ├── select.js
│ └── templates.js
├── index.html
└── styles.css
```
> درخت بالا دقیقاً همان چیزی است که باید در آرشیو آپلود کنید. پوشهٔ `scripts/` و دو فایل `index.html` و `styles.css` باید در ریشهٔ فایل زیپ باشند، نه داخل یک پوشهٔ اضافه.
>
> فایل تازهای نسازید؛ ماژولهای اضافه بارگذاری نمیشوند و اگر منطقی را داخلشان بگذارید از دست میرود.
- **توجه:** نام و مسیر فایلها را تغییر ندهید، هر گونه تغییر باعث دریافت نمره صفر از سیستم داوری خواهد شد.
- **توجه:** فایلها باید در **ریشهٔ** فایل زیپ باشند، نه داخل یک پوشهٔ اضافه. اگر همهچیز را داخل پوشهای به اسم `answer` بگذارید، مسیرهای موردانتظار پیدا نمیشوند و داوری اجرا نمیشود.
- **توجه: فایل تازهای نسازید.** سیستم داوری کوئرا فقط فایلهای بالا را در نظر میگیرد، پس اگر ماژول جدیدی بسازید و از جایی `import`اش کنید، آن ماژول در سیستم داوری در نظر گرفته نمیشود و بارگذاری کل صفحه شکست میخورد. کد کمکی را داخل همان فایلهای موجود بنویسید.
ارسال پاسخ برای این سؤال
در حال حاضر شما دسترسی ندارید.