Timeline
Хореография на одних часах: треки на любом числе целей — UI-элементах, 3D-узлах, источниках света,
камере, 2D-узлах и спрайтах — расставленные по абсолютному времени, плюс вызовы JS, метки и паузы,
всё под управлением одного хендла Animation. Собери
один раз и play() при каждом появлении. Для одной цели её собственные .animateTo() /
.animateFrom() принимают тот же набор пропов и возвращают тот же хендл; для свободного значения,
которое применяешь сам, используй animate.
Обзор
const title = UIText('LEVEL 1').style({ fontSize: 40, color: 'white', opacity: 0 })
const hint = UIText('Tap to start').style({ fontSize: 18, color: '#9fb0d0', opacity: 0 })
UIScreen(title, hint).style({ alignItems: 'center', justifyContent: 'center', gap: 12 }).open()
const intro = Timeline()
.add(title, { opacity: 1, transform: ['translateY(24px)', 'translateY(0px)'], duration: 500, easing: 'outCubic' })
.label('shown') // = текущий конец: 500 мс
.add(hint, { opacity: [0, 1, 0.4, 1], duration: 900 }, { at: ['shown', -200] }) // стартует на 300 мс
.call('shown', () => console.log('title in'))
intro.play()
intro.finished.then((done) => console.log('intro done', done)) // через 1200 мсСборка
Timeline(options?: {
clock?: 'ui' | 'game', // по умолчанию 'ui', какими бы ни были цели
loop?: boolean | number, // повторять весь таймлайн: true = вечно, n = циклов
loopMode?: 'ping-pong' | 'restart', // по умолчанию 'ping-pong'
delay?: number, // мс до первого цикла
}): Timeline
tl.add(target | targets[], props, { at?, stagger? }?): this // как target.animateTo(props)
tl.addFrom(target | targets[], props, { at?, stagger? }?): this // как animateFrom: собственное состояние цели — последний ключ
tl.animate(options, at?): this // трек свободного значения — опции animate()
tl.call(at, fn: () => void): this // выполнить JS, когда воспроизведение пересекает `at`
tl.label(name, at?): this // назвать момент времени; по умолчанию = текущий конец
tl.hold(ms): this // неподвижность после текущего конца
tl.labels: Record<string, number> // именованные моменты, мс
tl.end: number // текущий конец, мс
type TimelinePosition = number | string | [label: string, offsetMs: number]- Любое время — миллисекунды от начала таймлайна.
at— число, метка или[label, offset]; неизвестная метка бросает исключение. Безatтрек стартует в текущем конце, так что последовательныеaddиграют один за другим. props— наборanimateTo: проп → значение (одно значение твинится от текущего, массив — ключевые кадры), плюсduration(мс, по умолчанию 225),delay(мс послеat),easing(по умолчанию ease-in-out,cubic-bezier(0.42, 0, 0.58, 1)— см. изинги),times,commit.loop,loopModeиclockпринадлежат таймлайну, а не треку.- С массивом целей
staggerзапускает каждую на столько мс позже предыдущей. callвыполняетfn, когда воспроизведение пересекает его время в любом направлении (seek, отрицательныйrate), в порядке времени, до завершения таймлайна; вызов на 0 выполняется приplay().
onComplete трека tl.animate(…) не вызывается — используй tl.call(at, fn) или
tl.finished. onUpdate работает как в animate().
Что анимирует каждая цель
| цель | пропы |
|---|---|
| UI-элемент | любой проп стиля (opacity, width, bgColor, transform, …): числа, длины и цвета интерполируются, transform — по функциям списка, прочие строки переключаются на ключевом кадре; text на текстовых элементах (переключение) |
3D-узел (Mesh, Model, Node) |
position, scale, quaternion (slerp), eulerAngles (градусы, по осям — [0, 720, 0] делает два оборота) |
Light |
пропы узла + intensity, color |
камера (scene.camera) |
пропы узла + fov (градусы), near, far |
Node2D |
x, y, position, rotation (градусы), scale |
Sprite |
пропы 2D-узла + opacity, color |
3D / 2D цель пропускает проп, который не может анимировать, с однократным предупреждением. Как
твинятся UI-пропы (единицы, цвета, transform) — в Стилизации.
для векторных пропов — position, scale, quaternion, eulerAngles и color на 3D / 2D
целях — массив чисел это ОДНО значение; ключевые кадры — массив векторов:
scale: [[1, 1, 1], [1.2, 1.2, 1.2], [1, 1, 1]]. scale: 1.5 — равномерно.
трек пишет трансформ 3D-узла напрямую. Узел, который двигает физическое тело или контроллер персонажа, через его тело не проводится — анимируй обычный узел.
Воспроизведение
Таймлайн — это Animation: play(), pause(), resume(), seek(), finish(), cancel(),
time, progress, rate, finished: см.
хендл Animation. seek принимает и метку
(tl.seek('shown')).
- До
play()ничего не выполняется. Повторныйplay()перезапускает с 0 и завершаетfinishedпредыдущего запуска значениемfalse. - Поза в любой момент полностью определена треками: при
play()каждая цель прыгает на первый ключевой кадр своего трека — даже когда этот трек стартует позже — а после конца трека его последний ключевой кадр удерживается. - Канал принадлежит самой новой анимации: запуск таймлайна забирает названные им пропы у любой более старой анимации на тех же целях; их остальные пропы продолжают работать.
- Одноразовый трек фиксирует свой последний ключевой кадр при
play(): с этого моментаstyleUI-элемента,x/y/position/rotation/scale2D-узла,opacity/colorспрайта,intensityисточника света иnear/farкамеры читают итоговое значение, пока поза ещё движется. Трансформ 3D-узла иcamera.fovчитают живую позу;light.colorпродолжает читать значение, которое было до твина.
таймлайн идёт на часах 'ui', если не передать clock: 'game' — даже над 3D-узлами, чей
собственный animateTo по умолчанию на 'game'. Часы 'game' следуют Time.scale /
Time.paused на хосте с 3D-движком, когда существует 3D Scene; см.
Часы.
const scene = new Scene()
const sun = Light.sun({ intensity: 20000 })
const door = Mesh.box({ material: Material.lit({ color: '#8a6' }), position: [0, 1, -6], scale: [2, 2, 0.2] })
const gems = [-2, 0, 2].map((x) => Mesh.sphere({ material: Material.lit({ color: '#4f7cff' }), position: [x, 0.5, -3], scale: 0.4 }))
scene.add(sun, door, ...gems)
scene.open()
const opening = Timeline({ clock: 'game' }) // останавливается, пока Time.paused
.add(scene.camera, { position: [[0, 2, 6], [0, 1.5, 1]], fov: 40, duration: 1500 }, { at: 0 })
.add(door, { position: [0, 3.2, -6], duration: 800, easing: 'inOutCubic' }, { at: 600 })
.add(gems, { eulerAngles: [0, 360, 0], duration: 600 }, { at: 900, stagger: 120 }) // по одному обороту каждый
.add(sun, { intensity: 60000, color: '#ffd9a0', duration: 1200 }, { at: 300 })
.call(1500, () => console.log('cutscene over'))
opening.play()Подводные камни
// ✗ собрать и ждать — таймлайн ничего не делает, пока его не запустят
const fade = Timeline().add(card, { opacity: 1 })
// ✓ запусти его, и снова при каждом появлении
fade.play()
// ✗ числа на векторном пропе — ОДИН вектор: это масштабирует к (1, 1.2, 1)
Timeline().add(gem, { scale: [1, 1.2, 1] }).play()
// ✓ ключевые кадры векторного пропа — векторы
Timeline().add(gem, { scale: [[1, 1, 1], [1.2, 1.2, 1.2], [1, 1, 1]] }).play()Смотрите также
- animate — свободные значения, хендл
Animation, часы - Изинги — имена кривых,
cubicBezier,steps - Стилизация —
.animateTo()/.animateFrom()и пропы стиля элементов - Time —
Time.scale/Time.paused