# Камера — `$.camera`

В ядре камеры нет: сцена рисуется в координатах окна. Камера живёт здесь и
применяется **в момент отрисовки** — `$.gfx` умножает мировые координаты узлов
на матрицу камеры. Поэтому `.pos()` у узла всегда мировые координаты, а
экранные получаются через `$.camera.worldToScreen()`.

```js
$.camera.follow('#hero', { smooth: 8, deadzone: 24 });
$.camera.zoom(2.4);
$.camera.shake(0.35, 6);
$.camera.limits(0, 0, 3000, 800);        // не показывать пустоту за краем
$.camera.panTo(1200, 400, 0.8);          // плавный наезд (катсцена)
```

---

## 1. Методы

| Вызов | Смысл |
|---|---|
| `follow(target, opts?)` / `unfollow()` / `followed()` | слежение за узлом |
| `pos()` / `x()` / `y()` | положение камеры в мире |
| `at(x, y)` | поставить камеру мгновенно |
| `panTo(x, y, seconds?)` | плавный наезд к точке |
| `zoom(value?)` / `zoomTo(value, seconds?, easing?)` | зум и плавный зум |
| `rotation(value?)` / `rotateTo(value, ms?)` | поворот кадра (радианы) и плавный поворот |
| `shake(amount, seconds?)` | тряска (сила, время) |
| `limits(x, y, w, h)` | границы, за которые камера не выезжает |
| `deadzone(size?)` | зона, в которой цель может двигаться без сдвига камеры |
| `viewport(w, h)` | логический размер кадра для пересчёта |
| `split(n)` / `add` / `remove` / `views()` | несколько камер — см. [viewports.md](highlevel/viewports) |
| `worldToScreen(x, y)` / `screenToWorld(x, y)` | перевод координат |
| `isOnScreen(node, margin?)` | видно ли узел (для отсечения) |
| `cameraTransform()` | матрица камеры (для своих расчётов) |
| `kind()`, `yaw()`, `pitch()`, `eye()`, `fov()`, `fog()`, `look()`, `mouseLook()`, `info()` | вид Re2D (§5) |

Слежение сглажено: `smooth` — скорость подтягивания, `deadzone` — размер
«окна свободы» вокруг цели.

## 1.0. Несколько камер (сплитскрин)

```js
$.camera.split(2);                          // две камеры в половинах окна
$.camera.viewAt('p2', hero2.x, hero2.y);    // куда смотрит вторая
```

Подробности — [viewports.md](highlevel/viewports). Коротко: регион выражается зумом и
центром камеры, поэтому ни сциссор, ни отдельные цели не нужны, а камеры
рисуются в один батч кадра. Ограничения (спрайты на границе регионов, общий пост
и свет, ввод по главной камере) — там же, §4.

## 1.1. Поворот кадра

```js
$.camera.rotation(Math.PI / 4);     // повернуть кадр на 45°
$.camera.rotation();                // прочитать угол
$.camera.rotateTo(-Math.PI / 2, 600);   // плавно
```

Вращается **всё, что рисуется миром**: спрайты, текст, треугольники, слои,
частицы. Координаты узлов остаются **мировыми**: `.pos()` не меняется, физика,
лучи и пикинг работают как обычно — меняется только картинка.

Положительный угол поворачивает мир **по часовой стрелке** на экране.

`$.camera.worldToScreen()` и `screenToWorld()` учитывают поворот и остаются
взаимно обратными — за это отвечает одна общая функция `frameWorldToScreen`
(импортируется из `camera.js`): раньше отрисовка узлов считала камеру **своей**
формулой, и поворот сдвигал только свет и VFX, а сами узлы стояли на месте.
Это нашлось тестом по скриншотам (картинка не менялась) — теперь формула одна.

Поворот входит в `$.camera.snapshot()`/`restore()`, поэтому катсцены
возвращают камеру вместе с углом.

## 5. Вид камеры: Re2D

`$.camera.kind(Re2D)` переключает камеру на взгляд **от первого лица** над тем же
плоским миром ([RE2D.md](RE2D), [re2d.md](highlevel/re2d)). Без `kind` камера —
обычная 2D, и всё выше работает как раньше.

```js
$.camera.kind(Re2D).eye(48).fov(70).pitch(0).mouseLook(true);
$.camera.follow('#hero');            // глаза на теле, мгновенно
$.camera.look(12, -4);               // повернуть на сдвиг мыши (px), без захвата мыши
$.camera.info();                     // { kind, x, y, eye, yaw, pitch, fov, … } — углы в градусах
```

| Вызов | 2D | Re2D |
|---|---|---|
| `kind(name?)` | `'2d'` | `'re2d'` (или `kind(null)` — назад в 2D) |
| `at(x, y)` / `follow(sel)` | центр кадра | положение глаз на полу; слежение мгновенное |
| `rotation(rad)` | крен кадра | **куда смотрим** (yaw в радианах); у 2D-кадра и у Re2D свои углы — один не перетекает в другой |
| `yaw(deg)` | — | куда смотрим, градусы, диапазон (−180, 180]; всегда пишет угол взгляда, в какой бы вид ни была включена камера |
| `zoom(k)` | масштаб кадра | сужение угла обзора (тангенс половины угла делится на `k`) |
| `pitch(deg)` | — | наклон вверх-вниз, зажат в ±85° (настоящий поворот камеры) |
| `eye(h)` | — | высота глаз над полом, пиксели мира (48 по умолчанию) |
| `fov(deg)` | — | вертикальный угол обзора до зума, 5…170° (70 по умолчанию) |
| `fog(far, min)` | — | затемнение с расстоянием: цвет × `clamp(1 − d/far, min, 1)`; `fog(0)` выключает |
| `look(dx, dy)` | — | повернуть на сдвиг мыши в пикселях: вправо — направо, вверх — вверх |
| `mouseLook(on? \| { on, sensitivity })` | — | взгляд мышью: захват мыши (`$.window.mouseLock`) и поворот каждый кадр; чувствительность — радианы на пиксель (0.0025) |
| `worldToScreen(p)` | `{ x, y }` | `{ x, y, scale, depth, visible }`; высота — `p.z` (у узла `.depth`), по умолчанию пол |
| `screenToWorld(p)` | мировая точка | точка на полу (или на высоте `p.z`); `null`, если луч уходит в небо |
| `limits(...)` | границы камеры | не применяются |
| `shake(...)` | сдвиг кадра | небольшой поворот камеры (по `shake_x/shake_y`) |
| `snapshot()` / `restore()` | как раньше | дополнительно переносят `kind`, `yaw`, `pitch`, `eye`, `fov`, `fogFar`, `fogMin` |

Взгляд мышью детерминирован: сдвиг мыши за кадр попадает в запись
`--record/--replay` (поля `dx`/`dy`, [RECORD_REPLAY.md](RECORD_REPLAY)), а
`look(dx, dy)` можно вызывать из кода и тестов. Перспективу считает C
(`engine.re2d.*`, [internal/NATIVE.md](internal/NATIVE)), камера лишь собирает его параметры
(`re2dViewOf`): одна реализация математики.

Ограничения: дополнительные камеры (`split/add/pip`) остаются 2D; `isOnScreen`
в Re2D проверяет проекцию центра узла, а не его границы.

## 2. Связь с оружием и боевкой

`$.camera.shake()` принимает силу **отдачи**: `$.weapons.fire()` возвращает
`recoil`, его удобно отдавать камере и прицелу.

## 3. Ограничения

* **границы — прямоугольник**: сложная форма (полигон, несколько комнат) не
  поддержана;
* **тряска — смещение**, не угловая: крен кадра не делается (для крена —
  `rotateTo` с небольшой амплитудой самому);
* **границы (`limits`) считаются без поворота**: при наклонённом кадре видно
  чуть больше по диагонали, и у самой границы может показаться пустота;
* **сплитскрин — общий пост и ввод**: несколько камер поддержаны
  (`split/add/views`, §1.0 и [viewports.md](highlevel/viewports)), но пост-обработка и
  свет считаются на кадр целиком, а `mouseWorld()` — по главной камере, а не по
  региону под курсором ([viewports.md](highlevel/viewports) §4).

## 4. Отсечение

`isOnScreen` считает по границам узла с запасом `margin`: рисуйте только то, что
попало, — на больших мирах это главная экономия кадра.
