Vue предоставляет компонент <Transition> для анимации появления и исчезновения одного элемента:
<template>
<button @click="show = !show">Переключить</button>
<Transition name="fade">
<p v-if="show">Этот элемент анимируется</p>
</Transition>
</template>/* Классы автоматически добавляются/убираются Vue */
/* Начало появления / конец исчезновения */
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(-10px);
}
/* Во время появления */
.fade-enter-active {
transition: all 0.3s ease-out;
}
/* Во время исчезновения */
.fade-leave-active {
transition: all 0.2s ease-in;
}
/* Финальное состояние появления / начало исчезновения */
.fade-enter-to,
.fade-leave-from {
opacity: 1;
transform: translateY(0);
}Временная шкала для enter-перехода:
v-enter-from ------> v-enter-active ------> v-enter-to
(начало) (весь процесс) (конец)Для leave-перехода:
v-leave-from ------> v-leave-active ------> v-leave-to
(начало) (весь процесс) (конец)Для сложных анимаций (GSAP, Anime.js) используют JS-хуки:
<Transition
@before-enter="onBeforeEnter"
@enter="onEnter"
@after-enter="onAfterEnter"
@enter-cancelled="onEnterCancelled"
@before-leave="onBeforeLeave"
@leave="onLeave"
@after-leave="onAfterLeave"
:css="false"
>
<div v-if="show">Контент</div>
</Transition>function onEnter(el, done) {
// done() нужно вызвать, когда анимация завершена
gsap.from(el, {
opacity: 0,
y: -20,
duration: 0.3,
onComplete: done
})
}Для анимации списков используют <TransitionGroup>:
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
</TransitionGroup>.list-move, /* анимация перемещения (FLIP) */
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* Убираем из потока для корректного FLIP */
.list-leave-active {
position: absolute;
}**FLIP** (First, Last, Invert, Play) — алгоритм для производительных анимаций перемещения:
1. **First** — запомни позицию элемента ДО изменения
2. **Last** — примени изменение, запомни позицию ПОСЛЕ
3. **Invert** — примени transform, чтобы элемент выглядел как ДО изменения
4. **Play** — анимируй к нулевому transform (к позиции ПОСЛЕ)
Ключевая идея: мы анимируем transform (GPU-ускорение) вместо top/left (перерасчёт layout).
function flip(element) {
// First
const first = element.getBoundingClientRect()
// Применяем изменение (sort, reorder и т.д.)
doChange()
// Last
const last = element.getBoundingClientRect()
// Invert
const dx = first.left - last.left
const dy = first.top - last.top
element.style.transform = `translate(${dx}px, ${dy}px)`
element.style.transition = 'none'
// Play (в следующем кадре)
requestAnimationFrame(() => {
element.style.transition = 'transform 0.3s ease'
element.style.transform = ''
})
}<!-- Анимировать элементы при первом рендере -->
<Transition appear name="fade">
<div>Сразу анимируется при загрузке страницы</div>
</Transition>Реализация FLIP-анимации — техника, которую Vue использует в TransitionGroup для анимации перемещения элементов
// FLIP (First, Last, Invert, Play) — алгоритм для плавных анимаций
// без дорогостоящих перерасчётов layout.
// Используем только transform (GPU) вместо top/left (CPU).
function createFlipAnimator(container) {
let snapshots = new Map()
return {
// Шаг 1: FIRST — снять "снимок" позиций до изменения
snapshot() {
snapshots.clear()
const children = container.children || []
for (const child of children) {
if (child.getBoundingClientRect) {
snapshots.set(child, child.getBoundingClientRect())
}
}
console.log(`[FLIP] snapshot: ${snapshots.size} элементов`)
},
// Шаги 2-4: LAST + INVERT + PLAY — применить изменение и анимировать
animate(duration = 300) {
const children = container.children || []
const animations = []
for (const child of children) {
const first = snapshots.get(child)
if (!first || !child.getBoundingClientRect) continue
// LAST — позиция после изменения
const last = child.getBoundingClientRect()
const dx = first.left - last.left
const dy = first.top - last.top
if (dx === 0 && dy === 0) continue // элемент не переместился
// INVERT — "откат" к старой позиции через transform
child.style.transform = `translate(${dx}px, ${dy}px)`
child.style.transition = 'none'
// PLAY — анимируем к нулю (реальной позиции)
animations.push(new Promise(resolve => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
child.style.transition = `transform ${duration}ms cubic-bezier(0.4, 0, 0.2, 1)`
child.style.transform = ''
child.addEventListener('transitionend', () => {
child.style.transition = ''
resolve()
}, { once: true })
})
})
}))
console.log(`[FLIP] ${child.id || 'el'}: dx=${dx.toFixed(1)} dy=${dy.toFixed(1)}`)
}
return Promise.all(animations)
}
}
}
// --- Симуляция FLIP без реального браузера ---
// Создаём фейковые элементы с позициями
function makeEl(id, x, y, width = 100, height = 40) {
return {
id,
_pos: { left: x, top: y, right: x + width, bottom: y + height, width, height },
style: {},
getBoundingClientRect() { return this._pos },
addEventListener(ev, fn, opts) {
// Симулируем завершение перехода немедленно
setTimeout(fn, 0)
}
}
}
const items = [
makeEl('item-A', 0, 0),
makeEl('item-B', 0, 50),
makeEl('item-C', 0, 100),
]
const container = { children: items }
const animator = createFlipAnimator(container)
console.log('=== Начальное состояние ===')
console.log('A:', items[0]._pos.top, 'B:', items[1]._pos.top, 'C:', items[2]._pos.top)
// Шаг 1: снять снимок ДО изменения
animator.snapshot()
// Шаг 2: применить изменение (сортировка — меняем позиции)
console.log('\n=== Применяем сортировку (C, A, B) ===')
items[0]._pos.top = 50 // A переместился вниз
items[1]._pos.top = 100 // B переместился ещё ниже
items[2]._pos.top = 0 // C переместился наверх
// Шаги 3-4: INVERT + PLAY
console.log('\n=== FLIP анимация ===')
animator.animate(300).then(() => {
console.log('\n=== Анимация завершена ===')
console.log('Финальные transform (должны быть пустыми):')
items.forEach(el => console.log(` ${el.id}: "${el.style.transform || '(сброшен)'}"`))
})Vue предоставляет компонент <Transition> для анимации появления и исчезновения одного элемента:
<template>
<button @click="show = !show">Переключить</button>
<Transition name="fade">
<p v-if="show">Этот элемент анимируется</p>
</Transition>
</template>/* Классы автоматически добавляются/убираются Vue */
/* Начало появления / конец исчезновения */
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(-10px);
}
/* Во время появления */
.fade-enter-active {
transition: all 0.3s ease-out;
}
/* Во время исчезновения */
.fade-leave-active {
transition: all 0.2s ease-in;
}
/* Финальное состояние появления / начало исчезновения */
.fade-enter-to,
.fade-leave-from {
opacity: 1;
transform: translateY(0);
}Временная шкала для enter-перехода:
v-enter-from ------> v-enter-active ------> v-enter-to
(начало) (весь процесс) (конец)Для leave-перехода:
v-leave-from ------> v-leave-active ------> v-leave-to
(начало) (весь процесс) (конец)Для сложных анимаций (GSAP, Anime.js) используют JS-хуки:
<Transition
@before-enter="onBeforeEnter"
@enter="onEnter"
@after-enter="onAfterEnter"
@enter-cancelled="onEnterCancelled"
@before-leave="onBeforeLeave"
@leave="onLeave"
@after-leave="onAfterLeave"
:css="false"
>
<div v-if="show">Контент</div>
</Transition>function onEnter(el, done) {
// done() нужно вызвать, когда анимация завершена
gsap.from(el, {
opacity: 0,
y: -20,
duration: 0.3,
onComplete: done
})
}Для анимации списков используют <TransitionGroup>:
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
</TransitionGroup>.list-move, /* анимация перемещения (FLIP) */
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* Убираем из потока для корректного FLIP */
.list-leave-active {
position: absolute;
}**FLIP** (First, Last, Invert, Play) — алгоритм для производительных анимаций перемещения:
1. **First** — запомни позицию элемента ДО изменения
2. **Last** — примени изменение, запомни позицию ПОСЛЕ
3. **Invert** — примени transform, чтобы элемент выглядел как ДО изменения
4. **Play** — анимируй к нулевому transform (к позиции ПОСЛЕ)
Ключевая идея: мы анимируем transform (GPU-ускорение) вместо top/left (перерасчёт layout).
function flip(element) {
// First
const first = element.getBoundingClientRect()
// Применяем изменение (sort, reorder и т.д.)
doChange()
// Last
const last = element.getBoundingClientRect()
// Invert
const dx = first.left - last.left
const dy = first.top - last.top
element.style.transform = `translate(${dx}px, ${dy}px)`
element.style.transition = 'none'
// Play (в следующем кадре)
requestAnimationFrame(() => {
element.style.transition = 'transform 0.3s ease'
element.style.transform = ''
})
}<!-- Анимировать элементы при первом рендере -->
<Transition appear name="fade">
<div>Сразу анимируется при загрузке страницы</div>
</Transition>Реализация FLIP-анимации — техника, которую Vue использует в TransitionGroup для анимации перемещения элементов
// FLIP (First, Last, Invert, Play) — алгоритм для плавных анимаций
// без дорогостоящих перерасчётов layout.
// Используем только transform (GPU) вместо top/left (CPU).
function createFlipAnimator(container) {
let snapshots = new Map()
return {
// Шаг 1: FIRST — снять "снимок" позиций до изменения
snapshot() {
snapshots.clear()
const children = container.children || []
for (const child of children) {
if (child.getBoundingClientRect) {
snapshots.set(child, child.getBoundingClientRect())
}
}
console.log(`[FLIP] snapshot: ${snapshots.size} элементов`)
},
// Шаги 2-4: LAST + INVERT + PLAY — применить изменение и анимировать
animate(duration = 300) {
const children = container.children || []
const animations = []
for (const child of children) {
const first = snapshots.get(child)
if (!first || !child.getBoundingClientRect) continue
// LAST — позиция после изменения
const last = child.getBoundingClientRect()
const dx = first.left - last.left
const dy = first.top - last.top
if (dx === 0 && dy === 0) continue // элемент не переместился
// INVERT — "откат" к старой позиции через transform
child.style.transform = `translate(${dx}px, ${dy}px)`
child.style.transition = 'none'
// PLAY — анимируем к нулю (реальной позиции)
animations.push(new Promise(resolve => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
child.style.transition = `transform ${duration}ms cubic-bezier(0.4, 0, 0.2, 1)`
child.style.transform = ''
child.addEventListener('transitionend', () => {
child.style.transition = ''
resolve()
}, { once: true })
})
})
}))
console.log(`[FLIP] ${child.id || 'el'}: dx=${dx.toFixed(1)} dy=${dy.toFixed(1)}`)
}
return Promise.all(animations)
}
}
}
// --- Симуляция FLIP без реального браузера ---
// Создаём фейковые элементы с позициями
function makeEl(id, x, y, width = 100, height = 40) {
return {
id,
_pos: { left: x, top: y, right: x + width, bottom: y + height, width, height },
style: {},
getBoundingClientRect() { return this._pos },
addEventListener(ev, fn, opts) {
// Симулируем завершение перехода немедленно
setTimeout(fn, 0)
}
}
}
const items = [
makeEl('item-A', 0, 0),
makeEl('item-B', 0, 50),
makeEl('item-C', 0, 100),
]
const container = { children: items }
const animator = createFlipAnimator(container)
console.log('=== Начальное состояние ===')
console.log('A:', items[0]._pos.top, 'B:', items[1]._pos.top, 'C:', items[2]._pos.top)
// Шаг 1: снять снимок ДО изменения
animator.snapshot()
// Шаг 2: применить изменение (сортировка — меняем позиции)
console.log('\n=== Применяем сортировку (C, A, B) ===')
items[0]._pos.top = 50 // A переместился вниз
items[1]._pos.top = 100 // B переместился ещё ниже
items[2]._pos.top = 0 // C переместился наверх
// Шаги 3-4: INVERT + PLAY
console.log('\n=== FLIP анимация ===')
animator.animate(300).then(() => {
console.log('\n=== Анимация завершена ===')
console.log('Финальные transform (должны быть пустыми):')
items.forEach(el => console.log(` ${el.id}: "${el.style.transform || '(сброшен)'}"`))
})Реализуй функцию `animateNumber(element, from, to, duration)` которая анимирует изменение числа в `element.textContent` от `from` до `to` за `duration` миллисекунд. Используй `requestAnimationFrame` и easing-функцию `easeInOut` (плавный старт и конец). Функция должна возвращать `Promise`, который resolve когда анимация завершена.
Функция easeInOutCubic: если t < 0.5 верни 4*t*t*t, иначе верни 1 - Math.pow(-2*t+2, 3)/2. В tick вычисляй progress = Math.min((now - startTime) / duration, 1), применяй easing, затем current = from + (to - from) * easedProgress. Не забудь вызвать resolve() когда progress достиг 1.
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке