Движок анимаций на физике пружин. Ядро ~2 КБ gzip, ноль зависимостей, 9 фреймворков — и compositor-путь, на котором анимация продолжает движение, даже когда главный поток занят.
Lab Motion — headless-движок: чистая математика движения (аналитический spring-солвер, кейфреймы, инерция, FLIP) отделена от DOM. Рендер делает ваш колбэк или готовый биндинг; время инжектируется — прогоны воспроизводимы бит-в-бит.
pnpm add @labpics/motionNode ≥ 22, ESM и CJS, типы в комплекте. Фреймворк для биндинга — optional
peer. Git-установка не поддерживается (dist/ собирается, в репозитории его
нет) — установка из тарбола описана в справочнике.
Один вызов — каскад карточек едет пружиной:
import { animate } from '@labpics/motion/animate';
await animate('.card', { x: 240, opacity: 1 }, {
spring: { mass: 1, stiffness: 170, damping: 26 },
stagger: 40,
}).finished;Реактивное значение: новая цель в полёте подхватывает позицию и скорость — перехваты без рывка:
import { MotionValue } from '@labpics/motion';
const x = new MotionValue({ initial: 0, spring: { mass: 1, stiffness: 200, damping: 20 } });
x.onChange((v) => { el.style.transform = `translateX(${v}px)`; });
x.setTarget(240); // повторный setTarget в любой момент — продолжение, не телепортВ React — то же самое одним хуком:
import { useSpring } from '@labpics/motion/react';
function Card({ open }: { open: boolean }) {
const x = useSpring(open ? 240 : 0, { mass: 1, stiffness: 200, damping: 20 });
return <div style={{ transform: `translateX(${x}px)` }} />;
}Больше runnable-рецептов (drag с инерцией, FLIP, presence, скролл-сценарии, bottom sheet) — в docs/recipes.md.
- Пружины, не длительности. Замкнутая форма вместо покадровой симуляции: O(1) на кадр, честный overshoot, непрерывность C¹ при перехвате — движение продолжается из текущей позиции и скорости.
- Анимация переживает занятый главный поток. Автономный переход
компилируется в нативный WAAPI-план (CSS
linear()в Chromium/Firefox, адаптивные кадры в WebKit) и живёт на compositor-потоке. Ретаргет и хендофф обратно в живую пружину — без разрыва траектории. → docs/compositor.md - Платите только за импортированное. Каждая возможность — отдельный
субпуть; точный
sideEffects-allowlist, неиспользуемое вырезает tree-shaking, размер ядра отслеживается в CI. - 9 фреймворков, одно ядро. React, Preact, Vue, Svelte, Solid, Angular, Qwik, Lit, Web Components. Биндинг — тонкая прослойка; ядро про фреймворки не знает.
- Layout-анимации всех уровней. FLIP; вложенный projection — потомки и
border-radius не искажаются в полёте; smart-animate по
data-motion-key(жанр Figma); zero-configautoAnimate. → docs/projection.md, docs/smart.md - Жесты и мобильные поведения. Drag с инерцией и rubber-band, bottom sheet, carousel, pull-to-refresh, drag-dismiss — headless state machines, цель выбирается по положению и скорости. → docs/behaviors.md
- Компилятор на этапе сборки. Vite-плагин выпекает статические вызовы в готовые WAAPI-артефакты: солвер и парсер не попадают в бандл потребителя. → docs/compiler.md
- Доступность и SSR всерьёз.
prefers-reduced-motionменяет характер движения (снап/фейд), а не выключает его грубо; импорт любого субпутя SSR-safe;NaN/Infinityникогда не попадают в CSS — гарантии запечатаны фаззинг-тестами в CI.
Импорт — @labpics/motion (ядро) или @labpics/motion/<субпуть>.
Корневой экспорт + 41 субпутей (входов exports в package.json — 42):
| Группа | Субпути |
|---|---|
| Ядро анимации | ./nano (WAAPI ≤ 1 КБ), ./animate (one-liner фасад), ./frame, ./driver, ./compositor, ./compositor/stagger, ./waapi, ./auto |
| Значения и физика | ./value, ./spring, ./decay, ./easing, ./keyframes, ./stagger, ./timeline, ./presets, ./tokens, ./utils |
| Доменные эффекты | ./flip, ./projection, ./smart, ./gestures, ./behaviors, ./scroll, ./in-view, ./presence, ./svg, ./svg-morph, ./a11y, ./surface (приватный executor compiled-поверхности) |
| Биндинги | ./react, ./preact, ./vue, ./svelte, ./solid, ./angular, ./qwik, ./lit, ./wc |
| Build-tool | ./compiler/vite (плагин), ./compiler/runtime (исполнитель, вставляется плагином) |
Что даёт каждый субпуть — в справочнике docs/api.md.
import { CompositorSpring } from '@labpics/motion/compositor';
const panel = new CompositorSpring({
spring: { mass: 1, stiffness: 170, damping: 26 },
property: 'transform', from: 0, to: 240,
target: el, format: (v) => `translateX(${v}px)`,
apply: (val) => { el.style.transform = String(val); }, // только на fallback-пути
});
panel.start(); // браузер ведёт пружину без участия главного потока
panel.retarget(120); // дискретное прерывание: новая кривая из текущей точки
const live = panel.handoffToLive(); // палец перехватил — продолжает rAF-пружинаБез WAAPI контроллер прозрачно деградирует в живую rAF-пружину — публичный API один. Фазовая модель (когда compositor, когда главный поток), fallback-матрица и границы гарантий — docs/compositor.md.
import { smartTransition } from '@labpics/motion/smart';
// пометьте узлы: <div data-motion-key="card-3">…</div>
await smartTransition(container, () => {
reorderAndSwapLayout(); // мутируйте DOM как угодно
}).finished; // matched едут FLIP'ом, entered/exited — фейдамиContinuity переживает пересоздание узла: идентичность — строковый ключ, а не ссылка на элемент. → docs/smart.md
// vite.config.ts
import { motionCompiler } from '@labpics/motion/compiler/vite';
export default { plugins: [motionCompiler()] };Статический вызов ./nano компилируется в готовый артефакт на этапе сборки:
в бандл не попадают ни солвер, ни парсер — только крошечный исполнитель.
Непредставимый вызов консервативно остаётся как есть. Скоуп первого среза и
гарантии — docs/compiler.md.
import { duration, easing, spring, springFromDurationBounce } from '@labpics/motion/tokens';
duration.base; // 200 (мс)
spring.default; // { mass: 1, stiffness: 170, damping: 26 }
springFromDurationBounce(0.35, 0); // восприятие (duration, bounce) → физикаТипобезопасный словарь примитивов движения, значения запинены тестами как контракт. → docs/tokens.md
Ядро — до 2220 байт gzip, ./nano — до 1024; размер ядра контролируется в CI.
Актуальные числа не копируются в Markdown — их выдаёт pnpm size; методология
и правила сравнения — docs/benchmark.md.
./animate даёт знакомую one-liner форму; карта переноса конкретных вызовов и
список того, что не объединено, — docs/migration.md.
| Раздел | Что внутри |
|---|---|
| Рецепты | Runnable-интеграции: drag, FLIP, presence, скролл, bottom sheet |
| Справочник API | Все 41 субпуть: что даёт каждый вход |
| Архитектура | Слои движка, фазовая модель, инварианты, отвергнутые пути |
| Compositor | WAAPI-план, ретаргет, хендофф, fallback-матрица, поддержка браузеров |
| Projection | Вложенный FLIP: дерево узлов, C¹-перехваты, не-цели v1 |
| Smart-animate | Диф по data-ключу, ghost-протокол, реинкарнация узлов |
| Behaviors | Bottom sheet, carousel, pull-to-refresh, drag-dismiss |
| Токены | Словарь движения и каноническая пара (duration, bounce) |
| Компилятор | Build-time lowering: скоуп, гарантии, sourcemaps |
| Ошибки | Каталог кодов MotionParamError (LMddd) |
| Бенчмарки | Методология измерений и автоматический контроль размера |
| Миграция | Соответствие вызовов Motion JS и Anime.js |
Рабочий процесс, архитектурные границы и обязательные проверки качества — CONTRIBUTING.md. Уязвимости — приватно через SECURITY.md.
MIT