# Процедурный пиксель-арт — `$.proc`

Спрайты не рисуют руками: их **выращивают** из сида. Порт идей из audm-neko
(`districts_art.gd` — атлас улицы, машин и промзоны) и из общего подхода
«палитра → силуэт → детали → свет».

```js
$.proc.define('hero', {
    w: 16, h: 24, seed: 7,
    palette: 'wasteland',
    build: 'normal',                 // normal | heavy | thin | child
    parts: ['head', 'torso', 'arms', 'legs'],
    gear: ['belt', 'straps', 'pouch'],
    hold: 'rifle',                   // '' | rifle | bag
    dirs: 4,                         // направлений (кадров) в листе
});
$('#hero').sprite($.proc.toSprite('hero'));       // спрайт движка
const sheet = $.proc.sheet('hero');               // все направления в одной
```

---

## 1. Палитры

Палитра — это не набор цветов, а **рампы**: на каждый материал несколько
оттенков от тени к свету. Процедурный художник берёт из рампы, а не выдумывает
цвет, иначе спрайт выглядит случайным.

| Материал | Где используется |
|---|---|
| `skin` | голова, открытые части |
| `cloth` | торс, руки, ноги |
| `leather` | пояс, сумки, ремни |
| `metal` | противогаз, ствол, пряжки |
| `dark` | контур, обувь, волосы |
| `accent` | яркая деталь |

Готовые палитры: `wasteland`, `city`, `forest`. Неизвестная палитра заменяется
на `wasteland` — игра не падает из-за опечатки.

```js
$.proc.palettes();               // ['wasteland', 'city', 'forest']
$.proc.palette('city').cloth;    // рампа целиком
```

## 2. Что рисуется

Порядок жёсткий и повторяет работу художника:

1. **силуэт по частям** — голова, торс, руки, ноги; пропорции задаёт
   телосложение (`head`/`torso`/`legs` в долях высоты, `armWidth`, `shoulder`);
2. **детали** — пояс, ремни, сумка, разгрузка, противогаз, капюшон;
3. **что в руках** — ствол или сумка;
4. **свет сверху слева** — верхняя кромка светлее, нижняя темнее;
5. **контур** — вокруг непрозрачных пикселей, там где пусто;
6. **зеркало** по нечётному направлению.

Сид определяет волосы, материал торса, наличие сумки — то есть «того же
персонажа», но с вариациями. Один сид — один спрайт, всегда одинаковый.

## 3. Методы

| Вызов | Возвращает |
|---|---|
| `$.proc.define(art)` / `get(id)` / `has(id)` / `ids()` / `remove(id)` / `clear()` | реестр описаний |
| `$.proc.load(data)` | пачку описаний (объект `{ art: [...] }` или массив) |
| `$.proc.render(id, dir?)` | холст: `{ w, h, data: Uint8Array }` (RGBA) |
| `$.proc.pixels(canvas)` | сами пиксели |
| `$.proc.toSprite(id, dir?)` | спрайт движка (кешируется по описанию и направлению) |
| `$.proc.sheet(id)` | `{ canvas, sprite, cols, w, h }` — все направления в одной текстуре |
| `$.proc.opaque(id, dir?)` | сколько непрозрачных пикселей (для проверок) |

Чистые функции наружу: `createCanvas`, `putPixel`, `getPixel`, `fillRect`,
`fillEllipse`, `applyLight`, `applyOutline`, `mirrorCanvas`, `parseColor`,
`rampColor`, `palette`, `makeRandom`, `normalizeArt`, `renderArt`, `PALETTES`,
`BUILDS`.

## 4. Запись в текстуру

Спрайт уезжает в GPU без файла: `engine.textureFromPixels(w, h, pixels)`
принимает RGBA-пиксели и отдаёт id текстуры. `$.proc.toSprite` делает это сам
и кеширует результат; без движка (модульные тесты) он честно возвращает `null`.

## 5. Ограничения (честно)

* **нет анимации**: граф кадров (`$.cels`) решает, какой кадр показать, но
  процедурного листа «ходьбы» из одного описания ещё нет — направления только
  зеркалят рисунок;
* **нет оружия и предметов отдельно**: `hold` рисует силуэт, а не полноценный
  спрайт ствола;
* **нет теней и материалов**: свет — примитивный (кромка светлее/темнее), а не
  источник с направлением;
* **нет записи в PNG**: пиксели отдаются наружу, но сохранять их в файл игра
  должна сама;
* **размер ограничен разумным**: спрайты считаются на CPU в JS, для крупных
  ассетов (тайлмапы, большие атласы) это не путь;
* **`dirs` — это зеркало**, а не отдельные рисунки: вид сзади/сбоку не
  отличается по форме (см. первый пункт).

## 6. Проверка

```bash
# палитры и рампы, примитивы, силуэт, свет, контур, зеркало, детали, реестр
build/_deps/quickjs-build/qjs tests/js/proc_test.mjs
```
