# Matrix Engine Движок анимированного фона в стиле "цифрового дождя" из Matrix с двумя интерактивными "кибер-глазами" по центру экрана. Глаза открыты/моргают, когда VPN выключен, и закрыты (с изменённой цветовой палитрой и паттерном радужки), когда VPN включён — визуальный индикатор состояния защиты прямо в фоне лендинга. Реагирует на движение мыши, тач/свайп, наклон телефона (гироскоп), клики (шоковая волна + глитч-эффекты) и подстраивает качество рендера под реальный FPS устройства. ## Архитектура ``` Rust (физика/симуляция) --WASM--> Web Worker (JS) --WebGL2--> ``` Три слоя, каждый со своей причиной существования: 1. **Rust -> WebAssembly** (`src/lib.rs`). Вся симуляция — движение капель, параллакс, взаимодействие с курсором/тачем/наклоном, ударные волны, глитчи, логика взгляда и моргания глаз — считается в Rust и компилируется в WASM. Быстрый детерминированный PRNG и работа с плоскими `Vec` вместо объектов дают предсказуемую производительность на тысячах капель без пауз GC (в отличие от эквивалентной реализации на чистом JS). 2. **Web Worker** (`matrix_worker.js`). Владеет `OffscreenCanvas`, вызывает `engine.tick()` и рендерит результат через WebGL2. Всё это происходит **не на main thread** — тяжёлый цикл "физика + рендер" не блокирует основной поток страницы (React-рендер, обработку кликов по UI, скролл), даже если сам кадр матрицы просядет по FPS. 3. **WebGL2 + GPU-инстансинг**. Каждая капля — это не отдельный DOM-узел и не отдельный draw call, а один инстанс в `drawArraysInstanced`: тысячи символов рисуются одним вызовом на GPU, с данными на каждый инстанс (позиция, символ, яркость, масштаб), приходящими прямо из памяти WASM. Итог: тяжёлые вычисления идут в нативно-скомпилированном Rust, тяжёлый рендер — на GPU одним батчем, а JS на воркере выступает тонкой склейкой между ними и не трогает основной поток вовсе. ## Файлы | Файл | Роль | |---|---| | `Cargo.toml` | Манифест Rust-крейта `matrix-engine` (cdylib+rlib), зависимости `wasm-bindgen`, `console_error_panic_hook`. | | `src/lib.rs` | Вся физика/симуляция на Rust: `FastRng` (xorshift32), структуры `Drop`/`GlitchBlock`/`Shockwave`/`GlassSymbol`, главная структура `MatrixEngine` с методом `tick()`. Пишет результат каждого кадра в плоские `Vec` буферы, которые JS читает напрямую из линейной памяти WASM (см. раздел ниже). | | `NetrunnerMatrix.tsx` | React client-компонент. Создаёт ``, один раз передаёт его в Web Worker через `transferControlToOffscreen()`, дальше управляет воркером message-протоколом (см. таблицу ниже) и слушает DOM-события (мышь/тач/гироскоп/видимость вкладки/ресайз контейнера). | | `matrix_worker.js` | Код, реально исполняющийся в Web Worker. `initWebGL()` собирает 3 шейдерные программы, `buildAtlas()` рисует текстурный атлас символов на `OffscreenCanvas` 2D, `updateDynamicScaling()` адаптирует разрешение канваса под реальный FPS, `render()` — главный `requestAnimationFrame`-луп (рендер матрицы+глитчей в offscreen framebuffer, затем полноэкранный пост-процесс). `self.onmessage` — приёмная сторона message-протокола. | | `gl-utils.js` | Небольшие хелперы компиляции шейдеров и линковки WebGL-программ (`compileShader`, `createProgram`) с логированием ошибок компиляции/линковки. | | `shaders.js` | Все GLSL ES 3.00 шейдеры как template-строки: `vsMatrix`/`fsMatrix` (рендер символов-капель с лёгким притяжением к курсору), `vsGlitch`/`fsGlitch` (цветные глитч-блоки), `vsOverlay`/`postProcessSource` (полноэкранный пост-процесс — CRT-дисторсия, хроматическая аберрация, шум/тиры и процедурный SDF-рендер двух кибер-глаз). | | `pkg/` | **Сгенерировано** `wasm-pack build`. Не редактировать вручную — перезаписывается при каждой сборке. | | `public/wasm-matrix/` (в корне проекта) | **Сгенерировано.** Финальное место, откуда браузер грузит WASM-модуль и JS-файлы движка в рантайме (см. раздел "Сборка"). | `repomix-output.xml` и `Cargo.lock` — служебные файлы, не требуют документирования и не должны редактироваться вручную. ## Message-протокол: `NetrunnerMatrix.tsx` <-> `matrix_worker.js` Main thread -> Worker: | Тип | Когда отправляется | Назначение | |---|---|---| | `INIT` | Один раз при монтировании компонента | Передаёт `OffscreenCanvas` (transferable) и стартовые параметры: размеры, `pixelScale` (DPR), `isMobile`, `isVpnOn`, `isDarkMode`, `fontSize`, путь до ассетов и т.д. | | `PAUSE` / `RESUME` | По событию `visibilitychange` документа | Останавливает/возобновляет `requestAnimationFrame`-луп, когда вкладка уходит в фон — экономит батарею и CPU/GPU. | | `RESIZE` | По `ResizeObserver` контейнера | Пересчитывает физический размер канваса, DPR-скейл, пересоздаёт капли под новую ширину (`engine.resize()`) и перестраивает атлас. | | `THEME` | При смене пропсов `isSecure`/`isDarkMode` | Сообщает воркеру новое состояние VPN и тему — перекрашивает атлас символов и запускает VPN-тир-глитч на экране. | | `TILT` | По `deviceorientation` (гироскоп телефона) | Передаёт нормализованный наклон по оси `gamma` для лёгкого 3D-параллакса капель. | | `SHOCKWAVE` / `DRAW_START` / `DRAW_END` / `KINETIC_SWIPE` / `MOUSE_MOVE` | По pointer/touch-событиям на `window` | Интеракции указателя: ударная волна и глитч-всплеск от клика/тапа, кинетический свайп и обычное перемещение курсора (влияют на положение символов и взгляд глаз). | Worker -> Main thread: | Тип | Когда отправляется | Назначение | |---|---|---| | `READY` | После загрузки WASM-модуля и первого прогретого кадра | Сигнал компоненту, что можно плавно проявить канвас (`opacity` transition), избегая вспышки нерасставленных капель. | ## Протокол памяти WASM <-> JS `MatrixEngine::tick()` каждый кадр перезаписывает несколько плоских `Vec` буферов внутри WASM. Вместо того чтобы возвращать их в JS через `wasm-bindgen` (что означало бы копирование всего буфера в JS-кучу 60 раз в секунду), движок экспортирует пары методов `*_ptr()` / `*_len()`: - `*_ptr()` — сырой указатель (`*const f32`) на начало буфера в линейной памяти WASM; - `*_len()` — актуальная длина в элементах `f32` (не в байтах). JS-сторона (`matrix_worker.js`) оборачивает эту память в `Float32Array` напрямую поверх `wasm.memory.buffer`, без копирования, и сразу заливает во WebGL-буфер через `bufferSubData`. Такой `TypedArray` валиден только до следующего вызова `engine.tick()` (рост внутреннего `Vec` в Rust может переместить память) — поэтому view создаётся заново на каждый кадр и не хранится между кадрами. Буферы и их stride (шаг полей на один элемент): | Буфер | `ptr()`/`len()` | Stride | Поля на элемент | |---|---|---|---| | Капли дождя | `render_ptr()` / `render_len()` | 5 | `x, y, char_idx, atlas_row, scale` | | Глитч-блоки | `glitch_ptr()` / `glitch_len()` | 5 | `x, y, w, h, color_type` | | "Стеклянные" символы | `glass_ptr()` / `glass_len()` | 4 | `x, y, char_idx, alpha` | | Состояние глаз | `eye_ptr()` / `eye_len()` | 6 (фикс.) | `cx, cy, gaze_x, gaze_y, 0.0 (резерв), eye_closedness` | Для капель `char_idx` и `atlas_row` вместе выбирают конкретный глиф в текстурном атласе символов (11 строк: 0..9 — градации яркости символа, 10 — полностью белая "голова" капли), который `buildAtlas()` заранее отрисовывает на `OffscreenCanvas` 2D в `matrix_worker.js`. ## Сборка Из папки `matrix-engine/`: ```bash wasm-pack build --target web --release ``` Либо через готовый скрипт из корня проекта (собирает WASM и раскладывает воркер/шейдеры/хелперы рядом с ним): ```bash ./build-wasm.sh ``` Результат (`pkg/` + скопированные `matrix_worker.js`, `shaders.js`, `gl-utils.js`) кладётся в `public/wasm-matrix/` в корне проекта — оттуда браузер грузит WASM-модуль и JS-файлы движка в рантайме (см. `assetsPath` в `NetrunnerMatrix.tsx`, по умолчанию `/wasm-matrix`). В Docker-образе аналогичная сборка (`wasm-pack build --target web --release` + копирование JS-файлов в `public/wasm-matrix/`) выполняется отдельным стейджем в корневом `Dockerfile`. ## Дополнительные визуальные слои (депт-сцена и стекло) Поверх исходной триады "Rust → Worker → WebGL2" добавлены два независимых визуальных слоя, оба не трогают физику дождя/глаз: 1. **Фон-глубина (дорога/машины)** — `fsBackground` в `shaders.js`, подключён в `matrix_worker.js` (`bgProgram`/`vaoBg`). Рисуется ПЕРВЫМ внутри `postFramebuffer`, до капель дождя, тем же `vsOverlay`-quad'ом, что и постпроцесс. Это единственный способ дать дождю "глубину сзади": канвас матрицы создаётся с `alpha:false` и каждый кадр полностью перезаписывается, поэтому DOM-слой позади канваса был бы просто не виден — сцена должна рисоваться внутри того же фреймбуфера, ДО капель, чтобы дождь (уже блендящийся поверх фона) частично её перекрывал, а не скрывал целиком. Минималистичная сцена: светящийся горизонт, дорога с перспективой к точке схода (асфальт — цвет фона с мелким зернистым шумом) и стилизованные машины, едущие в противоположных полосах (эффект двустороннего движения). Все формы аспект-корректны (см. `aspect` в шейдере) — не искажаются при смене соотношения сторон экрана. Цвет акцента синхронизирован с палитрой капель/глаз (циан выкл / фиолет вкл). 2. **Стекло** — `matrix-engine/GlassShatter.tsx`, отдельный React-компонент ВНЕ воркера и WASM (простой fullscreen-quad на main thread, GPU-нагрузка пренебрежимо мала). Рендерится, в отличие от фона, обычным DOM-канвасом ПОВЕРХ канваса матрицы — сюда ограничение `alpha:false` не применяется, оно работает только "сквозь" элемент, а не поверх него. Показывается только когда `active` (= `isVpnOn`, "глаза закрыты"). Клик/тап по панели добавляет трещины (Voronoi-ячейки + growing-радиус вокруг точки удара, пул из 6 слотов по аналогии с `Shockwave` в `lib.rs`); после `SHATTER_AFTER_HITS` ударов стекло осыпается и через паузу собирается заново — эффект зациклен.