# `$.layers` — канвас-слои, параллакс и затемнение

Подсистема добавляет к `$` **канвас-слои** (аналог `CanvasLayer` в Godot 4),
**параллакс** (аналог `ParallaxBackground`/`ParallaxLayer`) и
**полноэкранный оттенок/затемнение** (аналог `CanvasModulate` + переход
между сценами).

```js
$.ready(() => {
    // Дальний план: слой с параллаксом — его дети наследуют коэффициент.
    const bg = $.layers.create({ name: 'bg', order: -10, parallax: 0.5 });
    $('<sprite>', { id: 'mountains', sprite: 'mountains.png' }).at(0, 300).appendTo(bg);

    // Обычный мир — без слоя.
    $('<player>', { id: 'hero' }).at(200, 300).appendTo($.world);

    // Слой поверх мира.
    const hud = $.layers.create({ name: 'hud', order: 20 });
    $('<ui.label>', { text: 'HP' }).at(40, 24).appendTo(hud);

    // Ночной оттенок и переход в чёрное.
    $.layers.modulate('#0a1430', 0.35);
    $.layers.fadeOut(400).then(() => $.scene.load('level2'));
});
```

Слой — это **обычный узел** `<layer>`, поэтому его видят селекторы, твины и
агентский снимок. Порядок отрисовки берётся из существующего поля
`node.layer` (`render.js` сортирует по `layer`, затем по `depth`): при
добавлении ребёнка в слой подсистема проставляет ему порядок слоя, а при
смене порядка обновляет всё поддерево. `render.js` при этом не правится.

---

## Тег `<layer>`

```js
$('<layer>', { name: 'bg', order: -10, parallax: 0.5, visible: true, modulate: '#0a1430' });
```

| Поле | Тип | По умолчанию | Смысл |
|---|---|---|---|
| `name` | строка | `layer<uid>` | имя в реестре `$.layers` |
| `order` | число | `1` | порядок слоя (поле `node.layer`); больше — выше |
| `parallax` | число | — | коэффициент параллакса: `0` — приколот к экрану, `1` — как мир |
| `visible` | bool | `true` | видимость; `false` прячет и всех потомков |
| `modulate` | цвет | — | полноэкранный оттенок слоя (`#rgb`, `#rrggbbaa`, число) |

**Контейнер не трансформирует детей.** В движке нет наследования трансформа
родителя: `x/y/scale/angle` узла-слоя на детей не действуют, координаты
детей — мировые. Слой группирует только порядок и видимость (и параллакс
через раздачу коэффициента).

Порядок **внутри** слоя задаётся `.depth()` детей (`.layer(n)` ребёнка
подсистема каждый кадр переписывает на порядок слоя — это её служебное
поле).

---

## `$.layers`

### `$.layers.create(opts)` → обёртка слоя

Создаёт `<layer>` с полями из `opts` (см. таблицу выше) и возвращает обёртку
узла: `.appendTo(layer)` кладёт детей в слой.


### Якорь параллакса фиксируется в момент создания узла

Коэффициент `parallax` не хранит формулу «где узел должен быть»: подсистема
запоминает **якорь** — позицию на текущем кадре — и дальше двигает узел так,
чтобы он отставал от камеры ровно на `k`.

Из этого следует практическое правило: **ставьте фон после того, как камера
встала на игрока**. Если создать слой раньше (например, в `$.ready`, а
`$.camera.follow(...)` вызвать следом), якорь зафиксируется по камере в
положении «до», и слой останется приколотым к тому кадру — визуально фон
уедет или пропадёт совсем.

```js
$.ready(() => {
    // Камера сначала…
    $.camera.follow('#hero', { zoom: 3 }).limits(0, 0, W, H);
    // …и только потом фон. На первом кадре камера ещё переезжает на цель.
    let frames = 0;
    $.update((dt) => {
        if (++frames === 3 && !globalThis.__bg_made) {
            const far = $.layers.create({ name: 'far', order: -30, parallax: 0.12 });
            globalThis.__bg_made = true;
            $('<sprite>').sprite('bg_far.png').size(900, 281).at(400, 820).appendTo(far);
        }
    });
});
```

Слой не тайлится: одиночное полотно, поставленное в центре уровня, просто
останется далеко за кадром. Кладите несколько копий вдоль всего пути игрока —
с мировым шагом, равным ширине полотна.


### `$.layers.get(name)` → обёртка

Обёртка слоя по имени; пустая обёртка (`length === 0`), если слоя нет.

### `$.layers.has(name)` → bool

Есть ли слой с таким именем.

### `$.layers.list()` → массив имён

Имена слоёв **в порядке отрисовки**, снизу вверх.

### `$.layers.order(...)`

| Вызов | Результат |
|---|---|
| `order()` | массив `{ name, order }` снизу вверх |
| `order(name)` | число — порядок слоя (или `null`) |
| `order(name, n)` | задать порядок слоя, вернуть `$.layers` |
| `order(n)` | задать порядок верхнего слоя |

Смена порядка тут же переписывает `node.layer` у всех потомков слоя.
Прямой `.layer(n)` на узле-слое из ядра тоже считается сменой порядка слоя.

### `$.layers.current()` → string \| null

Имя верхнего (последнего по порядку) слоя.

### `$.layers.of(nodeOrSelector)` → string \| null

Имя слоя, которому принадлежит узел (сам слой тоже считается). Для узла вне
слоёв — `null`.

### `$.layers.show(name)` / `hide(name)` / `toggle(name)` → bool

Видимость слоя. `hide` гасит и всех потомков; `show` возвращает видимость
всем потомкам. `toggle` переключает по текущему состоянию. Возвращают
`true`, если слой найден.

> Видимость наследуется «сверху вниз»: скрытый слой каждый кадр прячет
> позже добавленных детей. Индивидуально скрытый ребёнок остаётся скрытым,
> пока слой видим, но `show(layer)` покажет всех — своих флагов подсистема
> не помнит.

### `$.layers.remove(name)` → bool

Удаляет слой вместе с детьми (как `Node.destroy()`).

### `$.layers.clear()` → `$.layers`

Удаляет все пользовательские слои. Служебный слой глобального оттенка
(`@overlay`) остаётся.

### `$.layers.bringToFront(nameOrNode)` / `sendToBack(nameOrNode)` → string \| null

Поднимает/опускает слой выше/ниже всех остальных. Принимает имя слоя, узел,
обёртку или селектор; для обычного узла берётся его слой. Возвращает имя слоя
или `null`.

### `$.layers.parallax(nodeOrSelector, factor)` → `$.layers` \| число

| Вызов | Результат |
|---|---|
| `parallax(node, factor)` | задать коэффициент, вернуть `$.layers` |
| `parallax(node)` | прочитать коэффициент (число или `null`) |
| `parallax(node, null)` | снять параллакс |

Механика: узел каждый кадр получает
`x = anchor_x + cam.x * (1 - f)` (аналогично `y`). Якорь фиксируется в момент
назначения; если игру узел сдвинула сама (телепорт, `.moveTo()`), якорь
перезакрепляется от новой позиции — параллакс не «съедает» игровое движение.

Если `nodeOrSelector` — слой, коэффициент раздаётся и потомкам (вложенные
слои рулят собой сами).

**Узлы с физическим телом не двигаются:** позицией управляет Box2D.
Подсистема пропускает их и один раз пишет предупреждение в журнал.

### `$.layers.modulate(color, alpha)` → `$.layers` \| `{ color, alpha }`

Общий полноэкранный оттенок поверх мира. Без аргументов — чтение. `null` —
выключить. Если `alpha` не задана, берётся альфа самого цвета.

### `$.layers.fade(color, alpha)` → `$.layers`

Мгновенно задаёт полноэкранное затемнение. Без аргументов — чтение
`{ color, alpha }`.

### `$.layers.fadeTo(color, alpha, ms)` → Promise

Плавно меняет затемнение за `ms` мс игрового времени (`$.time`), кадр не
блокируется. Promise разрешается по завершении (а также если начат новый
переход). Старый незавершённый переход отпускается, а не зависает.

### `$.layers.fadeOut(ms)` → Promise

`fadeTo('#000000', 1, ms)`; по умолчанию 400 мс.

### Метод узла `.parallax(f)`

`$('#star').parallax(0.5)` — то же, что `$.layers.parallax($('#star'), 0.5)`.
Без аргумента — чтение; `null` — снять.

---

## Чистые функции

Экспортируются для юнит-тестов (`tests/js/layers_test.mjs`):

```js
import { parallaxOffset, layerSortKey } from './src/highlevel/layers.js';

parallaxOffset(anchor, camValue, factor); // anchor + camValue * (1 - factor)
layerSortKey(layer, depth);               // layer * 1e6 + depth; принимает и узел
```

`layerSortKey` повторяет порядок `render.js` «слой важнее глубины» и годится
для `|depth| < 500000`.

---

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

| Чего нет | Почему |
|---|---|
| **Умножения по умолчанию** | сам `modulate` — это **альфа-наложение**: тёмные цвета затемняют, светлые высветляют, `alpha` — сила. Режим задаётся `.blend(name)` на узле-слое: `multiply` даёт честное затемнение (ночь), `add` — засветку (вспышка, молния) |
| **Своего шейдера у слоя** | слой — полноэкранный спрайт, у него нет шейдера: `.shader()` ставится на **узел** и на слой не переносится. Эффекты кадра — `$.gfx.post` (см. [render.md](highlevel/render)) |
| **Рендера слоя в текстуру** | слой не рисуется в render target, поэтому `modulate` накрывает всё, что нарисовано **до** слоя, а не только его детей |
| **Наследования трансформа** | узел-контейнер не смещает детей: их координаты остаются мировыми |
| **Точной маски `modulate`** | полноэкранный спрайт в общем батче; подгоняйте порядок слоя или используйте `$.layers.modulate()` для всего кадра |
| **Затемнения интерфейса** | `<$ui.*>` рисуется отдельным проходом после мира, поэтому `fade`/`modulate` его не накрывают |
| **Параллакса на телах** | позицией тела управляет Box2D — узел пропускается с предупреждением |
| **Служебной глубины у `<layer>`** | узел-слой держит `depth = 1000000`, чтобы его `modulate` рисовался после детей; не задавайте `depth` слою вручную |

Вложенные слои: внутренний слой — самостоятельный контейнер, он сохраняет
свой порядок и не наследует порядок внешнего (как `CanvasLayer` внутри
`CanvasLayer`).

---

## Пример: параллакс-фон из трёх планов

```js
$.ready(() => {
    const far = $.layers.create({ name: 'far', order: -30, parallax: 0.2 });
    const mid = $.layers.create({ name: 'mid', order: -20, parallax: 0.5 });
    const near = $.layers.create({ name: 'near', order: -10, parallax: 0.8 });

    $('<sprite>', { sprite: 'sky.png' }).at(400, 300).scale(3).appendTo(far);
    $('<sprite>', { sprite: 'hills.png' }).at(400, 380).scale(2).appendTo(mid);
    $('<sprite>', { sprite: 'trees.png' }).at(400, 440).scale(1.5).appendTo(near);

    $('<player>', { id: 'hero' }).at(200, 300).controls('wasd').appendTo($.world);
    $.camera.follow('#hero', { smooth: 0.2 });
});
```

Слой `near` с `parallax: 0.8` едет почти как мир, `far` с `0.2` — заметно
медленнее, а `parallax: 0` приколол бы план к экрану (удобно для градиента
неба независимо от камеры).
