Popmotion предоставляет функциональную модель анимаций, в которой ключевую роль играют значения, трансформации и композиция потоков данных. При использовании TypeScript основной задачей становится строгая типизация потоков значений, конфигураций анимаций и функций преобразования, что позволяет выявлять ошибки на этапе компиляции и стабилизировать поведение анимационных цепочек.
В основе типизации лежит описание того, какие значения могут участвовать в анимации. Popmotion оперирует универсальным понятием значения (value), которое может быть числом, строкой, объектом или сложной структурой.
Типизация таких значений обычно строится через обобщения:
type ValueType = number | string | { [key: string]: any }
Однако более точный подход предполагает использование дженериков, отражающих конкретную структуру анимируемого состояния:
interface Position {
x: number
y: number
}
const position: Position = {
x: 0,
y: 0
}
Такой подход позволяет закрепить контракт между анимационным движком и данными состояния, исключая возможность передачи несовместимых значений в функции интерполяции.
Tween является базовой единицей временной интерполяции. В TypeScript он обычно описывается через параметризированный интерфейс конфигурации:
interface TweenProps<T> {
from: T
to: T
duration?: number
ease?: (t: number) => number
onUpdate?: (v: T) => void
}
При этом ключевым моментом является сохранение соответствия между
from и to. Тип T должен быть строго идентичен,
что предотвращает некорректные переходы между несовместимыми
структурами.
Пример строго типизированного tween:
const fade: TweenProps<number> = {
from: 0,
to: 1,
duration: 300,
onUpdate: (v) => {
const opacity: number = v
}
}
Для сложных объектов используется глубокая типизация:
const move: TweenProps<Position> = {
from: { x: 0, y: 0 },
to: { x: 100, y: 200 }
}
Spring-модель требует более сложной типизации, так как включает параметры физической симуляции и состояние системы.
interface SpringConfig<T> {
from: T
to: T
stiffness: number
damping: number
mass: number
velocity?: T
onUpdate?: (v: T) => void
}
Особенность типизации spring заключается в том, что
velocity должен быть совместим по структуре с типом T. Для
примитивов это число, для объектов — структура, повторяющая форму
состояния.
const spring: SpringConfig<number> = {
from: 0,
to: 1,
stiffness: 120,
damping: 20
}
Для векторных систем:
const springPosition: SpringConfig<Position> = {
from: { x: 0, y: 0 },
to: { x: 300, y: 400 },
stiffness: 150,
damping: 25,
velocity: { x: 0, y: 0 }
}
Интерполяция является ключевым механизмом преобразования значений во времени. В TypeScript она выражается через универсальные функции:
type Interpolate<T> = (input: number) => T
Более точная модель учитывает диапазоны входных значений:
interface Interpolator<T> {
(progress: number): T
range?: [number, number]
}
Для числовых значений:
const interpolateNumber: Interpolator<number> = (t) => {
return 0 + (100 - 0) * t
}
Для объектов:
const interpolatePosition: Interpolator<Position> = (t) => {
return {
x: 0 + (100 - 0) * t,
y: 0 + (200 - 0) * t
}
}
Типизация гарантирует, что форма возвращаемого значения сохраняется на протяжении всей анимации.
В реактивной модели Popmotion используется концепция изменяемого значения, которое может быть подписано на обновления.
interface MotionValue<T> {
get(): T
set(v: T): void
subscribe(fn: (v: T) => void): () => void
}
Дженерик T фиксирует тип состояния, что позволяет строить строго типизированные реактивные цепочки.
const x: MotionValue<number>
const position: MotionValue<Position>
При компоновке нескольких MotionValue важно сохранять соответствие типов в вычисляемых функциях:
const combined = (pos: MotionValue<Position>) => {
pos.subscribe(({ x, y }) => {
const sum: number = x + y
})
}
Функции трансформации позволяют преобразовывать поток значений в новые значения. В TypeScript они описываются как чистые функции с фиксированным входом и выходом.
type Transform<I, O> = (input: I) => O
Простейший пример:
const double: Transform<number, number> = (v) => v * 2
Композиция трансформаций требует сохранения согласованности типов:
const toString: Transform<number, string> = (v) => `${v}`
const length: Transform<string, number> = (v) => v.length
Композиция:
const pipeline = (v: number): number => {
return length(toString(double(v)))
}
TypeScript предотвращает ошибки при несоответствии промежуточных типов, что особенно важно в цепочках Popmotion-пайплайнов.
Keyframes представляют собой последовательность состояний с временными метками.
interface Keyframe<T> {
time: number
value: T
}
interface KeyframeAnimation<T> {
keyframes: Keyframe<T>[]
duration: number
}
Пример строго типизированной анимации:
const opacityFrames: KeyframeAnimation<number> = {
keyframes: [
{ time: 0, value: 0 },
{ time: 500, value: 1 },
{ time: 1000, value: 0 }
],
duration: 1000
}
Для объектов:
const motionFrames: KeyframeAnimation<Position> = {
keyframes: [
{ time: 0, value: { x: 0, y: 0 } },
{ time: 500, value: { x: 100, y: 50 } },
{ time: 1000, value: { x: 200, y: 0 } }
],
duration: 1000
}
Сложные анимационные системы требуют ограничения дженериков для предотвращения некорректных типов.
type Numeric = number
interface NumericTween<T extends Numeric | Position> {
from: T
to: T
}
Для объектов часто применяется constraint на структуру:
type VectorLike = {
x: number
y: number
}
function animate<T extends VectorLike>(v: T) {
return v.x + v.y
}
Popmotion активно использует композицию функций, что требует строгой типизации цепочек.
type Pipe<I, O> = {
(input: I): O
}
Расширенная композиция:
type Pipe2<A, B, C> = (input: A) => C
Пример цепочки:
const step1 = (v: number): number => v + 1
const step2 = (v: number): string => `${v}`
const step3 = (v: string): number => v.length
const pipeline = (v: number): number => step3(step2(step1(v)))
Каждый этап строго проверяется TypeScript, что делает невозможным несоответствие типов в цепочке трансформаций.
Внутренние структуры Popmotion могут быть типизированы через единый базовый контракт:
interface Animation<T> {
start: () => void
stop: () => void
subscribe: (fn: (v: T) => void) => () => void
}
Это позволяет унифицировать различные типы анимаций под единый интерфейс, сохраняя при этом строгую типовую безопасность.
const animation: Animation<Position>
const fadeAnimation: Animation<number>
Типизация гарантирует, что подписчики получают корректные значения без необходимости проверки runtime-структур.
В сложных системах типы Popmotion часто расширяются через доменные модели, отражающие бизнес-логику интерфейса.
interface UIState {
opacity: number
scale: number
position: Position
}
const uiAnimation: Animation<UIState> = {
start() {},
stop() {},
subscribe(fn) {
fn({
opacity: 1,
scale: 1,
position: { x: 0, y: 0 }
})
return () => {}
}
}
Такой подход позволяет строить полностью типизированные анимационные слои, где каждая трансформация состояния проверяется компилятором, а структура данных остаётся согласованной на всём протяжении жизненного цикла анимации.