Реактивные потоки данных

Popmotion строится вокруг идеи непрерывных потоков значений во времени, где анимация и взаимодействие представлены не как разовые процедуры, а как реактивные последовательности данных. Любое движение, жест или физическая симуляция в этой модели становится источником значений, которые можно трансформировать, комбинировать и направлять в другие источники поведения интерфейса.

Ключевой принцип — время не управляется напрямую, а наблюдается через поток значений. Это сближает архитектуру Popmotion с реактивным программированием, где данные текут, а не запрашиваются.


Базовая модель потока: Action

Основной строительный блок реактивной системы — action.

action представляет собой поток, который:

  • генерирует значения во времени
  • может быть запущен и остановлен
  • поддерживает подписку на обновления

Каждый action можно рассматривать как функцию, исполняемую с течением времени, а не мгновенно.

import { action } from 'popmotion';

const myStream = action(({ update }) => {
  let x = 0;

  const interval = setInterval(() => {
    x += 1;
    update(x);
  }, 100);

  return () => clearInterval(interval);
});

myStream.start({
  update: v => console.log(v)
});

Внутри этого механизма:

  • update эмитит новое значение
  • возвращаемая функция отвечает за teardown
  • start активирует поток

Таким образом, action становится универсальным интерфейсом для любых потоков данных.


Источники потоков данных

В Popmotion почти всё, что связано с движением, является источником реактивного потока.

Tween

tween генерирует значения на основе интерполяции:

  • от начального состояния
  • к конечному состоянию
  • за заданное время
import { tween } from 'popmotion';

const animation = tween({
  from: 0,
  to: 100,
  duration: 1000
});

animation.start({
  update: v => console.log(v)
});

Каждое значение — это сэмпл функции времени.


Spring

spring моделирует физическую систему:

  • масса
  • жесткость
  • демпфирование

Поток значений здесь не детерминирован по шагам времени, а определяется уравнением движения.

import { spring } from 'popmotion';

spring({
  from: 0,
  to: 300,
  stiffness: 200
}).start({
  update: v => console.log(v)
});

Это поток, где каждое новое значение зависит от предыдущего состояния системы.


Decay

decay создаёт поток затухающего движения:

  • стартовая скорость
  • коэффициент трения
import { decay } from 'popmotion';

decay({
  velocity: 500
}).start({
  update: v => console.log(v)
});

Здесь поток моделирует инерцию без конечной точки.


Pointer и события ввода

Потоки могут формироваться из пользовательского ввода:

  • pointer
  • mouse
  • keyboard
import { pointer } from 'popmotion';

pointer().start({
  update: ({ x, y }) => {
    console.log(x, y);
  }
});

Каждое событие ввода становится частью реактивного потока координат.


Value как источник состояния

value — это особый поток, который хранит текущее состояние и может быть изменён извне.

import { value } from 'popmotion';

const x = value(0);

x.update(10);
x.subscribe(v => console.log(v));

Он одновременно:

  • источник данных
  • хранилище состояния
  • наблюдаемая переменная

Подписка и жизненный цикл потока

Любой поток в Popmotion имеет жизненный цикл:

  1. создание
  2. запуск
  3. эмиссия значений
  4. остановка

Подписка осуществляется через start.

const stream = tween({ from: 0, to: 1 });

const controls = stream.start({
  update: v => console.log(v),
  complete: () => console.log('done')
});

controls.stop();

controls предоставляет управление жизненным циклом:

  • stop() — остановка потока
  • pause() — приостановка (в некоторых реализациях)
  • resume() — возобновление

Операторы преобразования потока

Реактивная модель становится мощной благодаря преобразованиям данных.

pipe

pipe позволяет последовательно применять функции трансформации.

import { pipe, clamp } from 'popmotion';

const transform = pipe(
  v => v * 2,
  clamp(0, 100)
);

Каждое значение проходит через цепочку операторов.


map-подобные преобразования

Функции преобразования часто используются для:

  • масштабирования
  • нормализации
  • изменения диапазона
const double = v => v * 2;

В реактивной модели это не разовая операция, а трансформация потока.


clamp

Ограничение диапазона значений:

import { clamp } from 'popmotion';

const safe = clamp(0, 1);

interpolate

Позволяет преобразовать числовой поток в другой диапазон или структуру.

const output = v => v / 100;

Комбинирование потоков

Реактивная архитектура раскрывается при объединении потоков.

merge

Объединение нескольких источников в один поток.

import { merge, pointer, mouse } from 'popmotion';

const combined = merge(pointer(), mouse());

Здесь оба источника становятся единым потоком событий.


chain

Последовательное выполнение потоков:

  • один поток завершился
  • запускается следующий
import { chain, tween, spring } from 'popmotion';

chain(
  tween({ from: 0, to: 100 }),
  spring({ from: 100, to: 0 })
);

композиция потоков

Потоки можно вкладывать друг в друга:

  • результат одного становится входом другого
  • создаются каскады реакций

Пример реактивного пайплайна

Типичная структура реактивного поведения:

  • вход: pointer
  • трансформация: нормализация координат
  • фильтрация: ограничение области
  • выход: анимация элемента
import { pointer, value, pipe, clamp } from 'popmotion';

const position = value({ x: 0, y: 0 });

const transform = pipe(
  ({ x, y }) => ({
    x: clamp(0, 300)(x),
    y: clamp(0, 500)(y)
  })
);

pointer().start({
  update: v => {
    const next = transform(v);
    position.update(next);
  }
});

Здесь поток ввода преобразуется в поток состояния UI.


Реактивное состояние через value

value выступает центральным узлом управления состоянием.

Особенности:

  • хранит последнее значение
  • может быть подписан
  • может быть изменён вручную
  • может участвовать в анимации
const opacity = value(1);

opacity.subscribe(v => {
  element.style.opacity = v;
});

При этом value может быть источником и приёмником одновременно, что позволяет строить двунаправленные потоки.


Паттерны построения интерфейсов

Потоковое управление стилями

Каждое CSS-свойство становится реактивной величиной:

  • transform
  • opacity
  • position
const x = value(0);

x.subscribe(v => {
  element.style.transform = `translateX(${v}px)`;
});

Связывание жестов и анимации

Поток ввода напрямую управляет физической моделью:

  • pointer → spring → value
pointer().start({
  update: ({ x }) => {
    spring({
      from: x,
      to: 0
    }).start({
      update: v => element.style.left = v + 'px'
    });
  }
});

Инверсия управления

Вместо императивного вызова функций:

  • система реагирует на данные
  • поведение описывается как поток трансформаций

Нюансы и ограничения модели потоков

Реактивные потоки требуют внимания к следующим аспектам:

Управление подписками

Отсутствие остановки потока приводит к:

  • утечкам памяти
  • лишним вычислениям

Частота обновлений

Некоторые источники генерируют значения:

  • на каждом кадре
  • на каждом событии мыши

Необходима фильтрация или throttling.


Детеминированность

Физические потоки (spring, decay) зависят от:

  • частоты обновления
  • временного шага

что влияет на воспроизводимость поведения.


Композиционная сложность

При большом числе потоков:

  • возрастает сложность трассировки
  • становится важной визуализация зависимостей

Архитектурное значение реактивной модели

Потоки данных формируют структуру, в которой:

  • UI перестаёт быть результатом последовательных вызовов
  • поведение описывается как сеть преобразований
  • время становится параметром функции, а не управляющим механизмом

В рамках Popmotion это позволяет объединить:

  • физические модели движения
  • пользовательский ввод
  • интерполяцию значений
  • композицию анимаций

в единую реактивную систему, где любое изменение входного сигнала мгновенно распространяется по графу потоков данных.