docs
This commit is contained in:
+125
-12
@@ -1,5 +1,11 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// Детерминированный быстрый генератор псевдослучайных чисел (xorshift32).
|
||||
/// Используется вместо `rand`, чтобы не тащить в WASM лишние зависимости и
|
||||
/// не платить за криптографическую случайность — здесь важна только скорость,
|
||||
/// так как `next_u32()` вызывается тысячи раз за кадр. Состояние не может
|
||||
/// быть равно 0 (иначе xorshift зациклится на нуле), поэтому `new()` подменяет
|
||||
/// нулевой seed на 1.
|
||||
struct FastRng {
|
||||
state: u32,
|
||||
}
|
||||
@@ -42,6 +48,14 @@ impl FastRng {
|
||||
}
|
||||
}
|
||||
|
||||
/// Одна "капля" символов матричного дождя. Живёт в своей колонке (`base_x`)
|
||||
/// и падает вниз по `y` в логических единицах (не в пикселях — реальная
|
||||
/// экранная позиция считается как `y * font_size * parallax_factor`, см. `tick()`).
|
||||
/// `offset_x`/`vel_x` — горизонтальное смещение и скорость смещения от точки
|
||||
/// покоя, которые дают капле "оттолкнуться" от мыши/свайпа/шоквейва и потом
|
||||
/// демпфированно вернуться на место (пружинная модель, см. блок с `-0.12 * offset_x`).
|
||||
/// `layer` задаёт параллакс-слой (0 = far, 1 = mid, 2 = near — но за пределами
|
||||
/// mobile-режима, где слой far отключён совсем).
|
||||
struct Drop {
|
||||
base_x: f32,
|
||||
y: f32,
|
||||
@@ -52,6 +66,9 @@ struct Drop {
|
||||
glitch_timer: f32,
|
||||
}
|
||||
|
||||
/// Непрозрачный цветной прямоугольник-осколок для эффекта глитча (VHS-подобный
|
||||
/// сбой изображения). Живёт `life` кадров и рендерится в отдельном шейдере
|
||||
/// (`vsGlitch`/`fsGlitch`), поэтому не связан со шрифтовым атласом капель.
|
||||
struct GlitchBlock {
|
||||
x: f32,
|
||||
y: f32,
|
||||
@@ -91,6 +108,12 @@ impl GlitchBlock {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Кольцевая ударная волна от клика/тапа/EMP-импульса. Пул из 5 штук
|
||||
/// переиспользуется (см. `shockwaves` в `MatrixEngine::new`) вместо
|
||||
/// динамического создания/удаления — `trigger_shockwave` просто ищет
|
||||
/// первый неактивный слот (`!s.active`). `radius` растёт каждый тик,
|
||||
/// `life` тикает вниз до 0, после чего волна деактивируется.
|
||||
struct Shockwave {
|
||||
x: f32,
|
||||
y: f32,
|
||||
@@ -100,6 +123,10 @@ struct Shockwave {
|
||||
active: bool,
|
||||
}
|
||||
|
||||
/// Декоративный "осыпающийся" символ, который на короткое время появляется
|
||||
/// рядом с курсором при взаимодействии (эффект битого стекла/осыпающихся
|
||||
/// пикселей). Не связан с основными каплями дождя и рендерится отдельным
|
||||
/// буфером (`glass_buffer`).
|
||||
struct GlassSymbol {
|
||||
x: f32,
|
||||
y: f32,
|
||||
@@ -109,8 +136,32 @@ struct GlassSymbol {
|
||||
velocity_y: f32,
|
||||
}
|
||||
|
||||
// Количество f32-полей на одну каплю в `instance_buffer`: x, y, char_idx,
|
||||
// atlas_row, scale. Должно совпадать со stride, который читает WebGL-сторона
|
||||
// в `matrix_worker.js` (`strideBytes = 5 * 4`) — при изменении набора полей
|
||||
// нужно синхронно поправить оба места.
|
||||
const STRIDE: usize = 5;
|
||||
|
||||
/// Главный движок симуляции матричного дождя и "глаз".
|
||||
///
|
||||
/// # Протокол памяти WASM <-> JS
|
||||
/// Все производимые за кадр данные (позиции капель, глитч-блоки, "стеклянные"
|
||||
/// символы, состояние глаз) пишутся в плоские `Vec<f32>` буферы
|
||||
/// (`instance_buffer`, `glitch_buffer`, `glass_buffer`, `eye_buffer`) с
|
||||
/// фиксированным шагом (stride) полей на один элемент. wasm-bindgen НЕ
|
||||
/// возвращает эти `Vec<f32>` напрямую в JS (это потребовало бы копирования
|
||||
/// всего буфера в JS-кучу на каждый кадр), вместо этого экспортируются пары
|
||||
/// методов `*_ptr()`/`*_len()`, возвращающие сырой указатель в линейную
|
||||
/// память WASM и длину в элементах. JS-сторона (`matrix_worker.js`) оборачивает
|
||||
/// эту память в `Float32Array` напрямую поверх `wasm.memory.buffer` без
|
||||
/// копирования и сразу заливает во WebGL-буфер через `bufferSubData`.
|
||||
/// Это горячий путь (60 раз в секунду на тысячах капель), поэтому
|
||||
/// избежание лишних аллокаций/копий здесь критично для производительности.
|
||||
///
|
||||
/// Важно: указатель, возвращённый `render_ptr()` и т.п., валиден только до
|
||||
/// следующего вызова любого `&mut self` метода движка (например, `tick()`),
|
||||
/// так как рост `Vec` может переместить буфер в памяти — JS обязан прочитать
|
||||
/// данные сразу после `tick()`, не сохраняя указатель между кадрами.
|
||||
#[wasm_bindgen]
|
||||
pub struct MatrixEngine {
|
||||
width: u32,
|
||||
@@ -124,12 +175,19 @@ pub struct MatrixEngine {
|
||||
glass_symbols: Vec<GlassSymbol>,
|
||||
glitch_blocks: Vec<GlitchBlock>,
|
||||
|
||||
// Буфер рендера: [x, y, char_idx, atlas_row, scale] (Stride = 5)
|
||||
// Буфер рендера капель: [x, y, char_idx, atlas_row, scale] на каждую
|
||||
// видимую каплю (Stride = STRIDE = 5). Переиспользуется между кадрами —
|
||||
// не пересоздаётся, а перезаписывается по индексу, чтобы не аллоцировать.
|
||||
instance_buffer: Vec<f32>,
|
||||
visible_drops: usize, // Счетчик видимых элементов
|
||||
visible_drops: usize, // Сколько капель реально записано в instance_buffer в этом кадре (после frustum culling)
|
||||
|
||||
// Буфер глитч-блоков: [x, y, w, h, color_type] на блок (Stride = 5).
|
||||
glitch_buffer: Vec<f32>,
|
||||
// Буфер "стеклянных" символов: [x, y, char_idx, alpha] на символ (Stride = 4).
|
||||
glass_buffer: Vec<f32>,
|
||||
// Буфер состояния глаз: [cx, cy, gaze_x, gaze_y, 0.0, eye_closedness].
|
||||
// Всегда ровно 6 элементов (один взгляд на пару глаз), 5-й слот зарезервирован
|
||||
// (сейчас всегда 0.0) — JS читает его по фиксированным индексам, см. eyeData[] в matrix_worker.js.
|
||||
eye_buffer: Vec<f32>,
|
||||
|
||||
mouse_x: f32,
|
||||
@@ -138,28 +196,33 @@ pub struct MatrixEngine {
|
||||
is_mobile: bool,
|
||||
is_vpn_on: bool,
|
||||
tick_count: u32,
|
||||
// EMP-импульс (триггерится вместе со шоквейвом в trigger_glitch): временно
|
||||
// "стирает" капли рядом с точкой удара (см. is_void/is_emp_ring/is_scrambled в tick()).
|
||||
emp_timer: f32,
|
||||
emp_x: f32,
|
||||
emp_y: f32,
|
||||
// Горизонтальная "трещина" экрана — редкий декоративный сбой не на мобильных.
|
||||
tear_y: f32,
|
||||
tear_timer: f32,
|
||||
|
||||
blink_timer: f32,
|
||||
last_interaction: f32,
|
||||
gaze_target_x: f32,
|
||||
// --- Состояние глаз: моргание и взгляд ---
|
||||
blink_timer: f32, // >0, пока веки закрыты в рамках обычного моргания
|
||||
last_interaction: f32, // Кадров с последнего движения мыши/тача — используется и для отсчёта моргания, и для "блуждающего" взгляда на мобильных
|
||||
gaze_target_x: f32, // Куда должен смотреть взгляд (обычно — позиция курсора/тача)
|
||||
gaze_target_y: f32,
|
||||
gaze_x: f32,
|
||||
gaze_x: f32, // Текущее сглаженное смещение зрачка (интерполируется к цели с инерцией)
|
||||
gaze_y: f32,
|
||||
eye_closedness: f32,
|
||||
eye_closedness_vel: f32,
|
||||
eye_anchor_y: f32,
|
||||
mobile_wander_timer: f32,
|
||||
eye_closedness: f32, // 0 = открыты, 1 = закрыты; демпфированная пружина к target_closedness
|
||||
eye_closedness_vel: f32, // "скорость" пружины eye_closedness
|
||||
eye_anchor_y: f32, // Y-координата центра глаз, приходит из DOM (см. set_eye_anchor / #eye-anchor в React)
|
||||
mobile_wander_timer: f32, // Таймер для автономного блуждания взгляда, когда на мобильном давно не было тача
|
||||
|
||||
tilt_x: f32,
|
||||
// --- Наклон телефона и кинетический свайп ---
|
||||
tilt_x: f32, // deviceorientation.gamma, нормализованный к [-1, 1] — используется для лёгкого 3D-параллакса капель
|
||||
tilt_y: f32,
|
||||
swipe_x: f32,
|
||||
swipe_y: f32,
|
||||
swipe_timer: f32,
|
||||
swipe_timer: f32, // >0 короткое время после свайпа — капли в этой точке отталкиваются, как от мыши
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
@@ -225,6 +288,10 @@ impl MatrixEngine {
|
||||
engine
|
||||
}
|
||||
|
||||
/// (Пере)генерирует всю популяцию капель под текущие width/font_size.
|
||||
/// Вызывается из `new()` и из `resize()` — сетка колонок целиком зависит
|
||||
/// от ширины экрана, поэтому при ресайзе капли просто создаются заново,
|
||||
/// а не переиспользуются. Кортеж на слой — `(layer_id, scale, min_speed, max_speed)`.
|
||||
fn init_drops(&mut self) {
|
||||
self.drops.clear();
|
||||
|
||||
@@ -333,6 +400,10 @@ impl MatrixEngine {
|
||||
self.update_mouse(x, y);
|
||||
}
|
||||
|
||||
/// EMP-импульс: сильный сбой (шоквейв + всплеск глитч-блоков + временная
|
||||
/// "дыра" в поле капель вокруг точки, см. is_void/is_emp_ring/is_scrambled
|
||||
/// в tick()). Вызывается воркером как один из двух случайных вариантов
|
||||
/// реакции на DRAW_START наравне с обычным trigger_shockwave.
|
||||
pub fn trigger_glitch(&mut self, x: f32, y: f32) {
|
||||
self.emp_timer = 40.0; // Чуть больше белое пятно
|
||||
self.emp_x = x;
|
||||
@@ -354,6 +425,14 @@ impl MatrixEngine {
|
||||
}
|
||||
self.update_mouse(x, y);
|
||||
}
|
||||
/// Главный шаг симуляции, вызывается воркером ровно раз на кадр рендера
|
||||
/// (~60 FPS). Пересчитывает всё состояние и заполняет буферы заново
|
||||
/// (instance/glitch/glass/eye) — движок не хранит "дельту" между кадрами,
|
||||
/// JS-сторона каждый раз читает свежий снимок через *_ptr()/*_len().
|
||||
/// Логически tick() состоит из блоков: обновление шоквейвов -> логика
|
||||
/// взгляда и моргания глаз -> редкие декоративные глитчи/трещины ->
|
||||
/// EMP-геометрия -> основной проход по каплям (движение, взаимодействия,
|
||||
/// запись в instance_buffer) -> обновление глитч-блоков и "стеклянных" символов.
|
||||
pub fn tick(&mut self) {
|
||||
self.tick_count = self.tick_count.wrapping_add(1);
|
||||
self.last_interaction += 1.0;
|
||||
@@ -363,6 +442,7 @@ impl MatrixEngine {
|
||||
self.glass_buffer.clear();
|
||||
self.eye_buffer.clear();
|
||||
|
||||
// --- Обновление активных ударных волн ---
|
||||
// Скорость расхождения волны снижена
|
||||
let sw_speed = if self.is_mobile { 3.0 } else { 8.0 };
|
||||
|
||||
@@ -379,6 +459,11 @@ impl MatrixEngine {
|
||||
}
|
||||
|
||||
// --- Логика Глаз (Остается прежней) ---
|
||||
// Взгляд плавно едет к gaze_target_{x,y} (позиция курсора/тача, либо
|
||||
// случайная точка "блуждания" на мобильных при простое), моргание —
|
||||
// это отдельная демпфированная пружина eye_closedness с VPN как
|
||||
// "постоянно закрытыми глазами" (это выключенный тоггл = закрытые глаза,
|
||||
// см. target_closedness ниже).
|
||||
let cx = self.width as f32 / 2.0;
|
||||
let cy = if self.eye_anchor_y > 0.0 {
|
||||
self.eye_anchor_y
|
||||
@@ -465,6 +550,12 @@ impl MatrixEngine {
|
||||
self.tear_timer -= 1.0;
|
||||
}
|
||||
|
||||
// EMP расширяется наружу по мере убывания emp_timer (25 кадров жизни),
|
||||
// формируя три концентрические зоны вокруг точки удара, проверяемые
|
||||
// ниже как quadrance (dist_sq) без sqrt ради скорости:
|
||||
// [0, emp_r80_sq) — is_void: капли полностью гасятся (эпицентр)
|
||||
// [emp_r80_sq, emp_r20_sq) — is_scrambled: капли получают случайный символ
|
||||
// [emp_r20_sq, emp_rad_sq) — is_emp_ring: яркое кольцо-фронт волны
|
||||
let emp_rad = (25.0 - self.emp_timer) * 10.0;
|
||||
let emp_rad_sq = emp_rad * emp_rad;
|
||||
let emp_r20_sq = if emp_rad > 20.0 {
|
||||
@@ -479,6 +570,13 @@ impl MatrixEngine {
|
||||
};
|
||||
|
||||
// --- Итерация по каплям ---
|
||||
// Для каждой капли: (1) горизонтальный wrap колонки при выходе за
|
||||
// экран, (2) frustum culling по Y с ре-спауном сверху/снизу,
|
||||
// (3) реакция на активные шоквейвы/мышь-свайп/EMP/трещину,
|
||||
// (4) пружинное затухание offset_x к нулю, (5) выбор символа и его
|
||||
// "яркости" (atlas_row) через хэш от индекса+Y+времени — эмулирует
|
||||
// случайное мерцание символов без re-roll каждого символа отдельно,
|
||||
// (6) запись итоговых 5 полей в instance_buffer.
|
||||
for i in 0..self.drops.len() {
|
||||
let drop = &mut self.drops[i];
|
||||
|
||||
@@ -610,6 +708,14 @@ impl MatrixEngine {
|
||||
drop.glitch_timer = 5.0;
|
||||
}
|
||||
|
||||
// Пространственный хэш (индекс капли + ячейка Y + медленно
|
||||
// тикающий сдвиг времени) вместо rng.next_u32(): даёт детерминированный
|
||||
// "случайный" символ для конкретной позиции капли, который стабилен
|
||||
// между кадрами (не мерцает каждый символ каждый тик) и меняется
|
||||
// только когда сама капля сдвигается на новую Y-ячейку либо раз
|
||||
// в 10 тиков через time_shift. 73856093/19349663 — стандартные
|
||||
// простые числа для склейки координат в hash-grid (частая практика
|
||||
// в процедурной генерации, минимизирует коллизии решётки).
|
||||
let cell_y = drop.y as i32 as u32;
|
||||
let time_shift = self.tick_count / 10;
|
||||
let hash = (i as u32)
|
||||
@@ -706,6 +812,13 @@ impl MatrixEngine {
|
||||
self.glass_symbols.retain(|s| s.life > 0.0);
|
||||
}
|
||||
|
||||
// --- Доступ к буферам для JS (см. doc-комментарий структуры MatrixEngine
|
||||
// про протокол памяти WASM<->JS). Каждый буфер — пара ptr()/len(): ptr()
|
||||
// отдаёт сырой указатель в линейную память WASM, len() — актуальную длину
|
||||
// в элементах f32 (не в байтах и не в количестве "объектов"). instance_buffer
|
||||
// физически может быть больше, чем нужно рендерить в этом кадре (ёмкость
|
||||
// растёт, но не всегда используется целиком), поэтому render_len() возвращает
|
||||
// не capacity буфера, а именно visible_drops * STRIDE.
|
||||
pub fn render_ptr(&self) -> *const f32 {
|
||||
self.instance_buffer.as_ptr()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user