Ты замечал, что одни анимации ощущаются «живыми», а другие — механическими? Карточка, которая «выскакивает» быстро и медленно тормозит — ощущается естественно. Карточка с одинаковой скоростью — роботизированно. Разница — в кривой Безье. В Figma, CSS transitions, After Effects, JavaScript-анимациях — везде используется cubic-bezier.
Анимация — это функция от времени: как быстро меняется значение от начала к концу. Линейная функция даёт одинаковую скорость, что выглядит неестественно. Кривая Безье позволяет задать разгон, торможение, «пружину» — всё что делает интерфейс живым.
animation-timing-functionCSS принимает cubic-bezier(x1, y1, x2, y2) — две контрольные точки кубической кривой Безье. Ось X — время (0 до 1), ось Y — значение свойства:
// Стандартные функции CSS — это конкретные cubic-bezier:
const ease = [0.25, 0.1, 0.25, 1.0] // медленно → быстро → медленно
const easeIn = [0.42, 0.0, 1.0, 1.0] // медленно → быстро
const easeOut = [0.0, 0.0, 0.58, 1.0] // быстро → медленно
const linear = [0.0, 0.0, 1.0, 1.0] // равномерно
const easeInOut = [0.42, 0.0, 0.58, 1.0] // медленно → быстро → медленноКубическая кривая Безье через 4 точки, параметр t ∈ [0, 1]:
// Линейная интерполяция — основа всего
function lerp(a, b, t) {
return a + (b - a) * t
}
// Квадратичная Безье (3 точки)
function quadraticBezier(p0, p1, p2, t) {
return lerp(lerp(p0, p1, t), lerp(p1, p2, t), t)
}
// Кубическая Безье (4 точки) — используется в CSS
function cubicBezier(p0, p1, p2, p3, t) {
const q0 = lerp(p0, p1, t)
const q1 = lerp(p1, p2, t)
const q2 = lerp(p2, p3, t)
return quadraticBezier(q0, q1, q2, t)
}В коде анимаций удобнее использовать простые формулы без вычисления CSS cubic-bezier:
const easing = {
linear: t => t,
easeOut: t => 1 - Math.pow(1 - t, 3), // быстро в начале
easeIn: t => Math.pow(t, 3), // медленно в начале
easeInOut: t => t < 0.5
? 4 * t * t * t
: 1 - Math.pow(-2 * t + 2, 3) / 2,
// "Пружина" с перебросом:
bounce: t => {
const c4 = (2 * Math.PI) / 3
return t === 0 ? 0 : t === 1 ? 1
: Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1
}
}steps(n) делит анимацию на N дискретных шагов. Используется для спрайт-анимаций:
function stepsEasing(numSteps) {
return t => Math.floor(t * numSteps) / numSteps
}
// steps(4): t=0.3 → 0.25 (прыжок), t=0.6 → 0.5 (прыжок)
// Нет плавности — только дискретные позицииОшибка 1: Линейная анимация для UI-элементов
// ПЛОХО — роботизированно, неестественно
element.style.transition = 'transform 0.3s linear'
// ХОРОШО — естественное движение
element.style.transition = 'transform 0.3s ease-out'Ошибка 2: Слишком долгие анимации
// Человек ждёт после 200ms — анимация мешает работе
// ПЛОХО: 0.8s для появления карточки
// ХОРОШО: 0.15-0.3s для появления, 0.1-0.2s для исчезновенияОшибка 3: Анимировать layout-свойства вместо transform
// ПЛОХО — вызывает reflow на каждый кадр
element.style.left = lerp(0, 200, t) + 'px'
// ХОРОШО — GPU, без reflow
element.style.transform = `translateX(${lerp(0, 200, t)}px)`d3.easing.cubicOut и др.Математика кривой Безье: lerp, easing-функции, сравнение анимаций, steps()
// Математика кривой Безье — чистый JS, без DOM
// ===== Базовые функции =====
function lerp(a, b, t) {
return a + (b - a) * t
}
function quadraticBezier(p0, p1, p2, t) {
return lerp(lerp(p0, p1, t), lerp(p1, p2, t), t)
}
function cubicBezierPoint(p0, p1, p2, p3, t) {
const q0 = lerp(p0, p1, t)
const q1 = lerp(p1, p2, t)
const q2 = lerp(p2, p3, t)
return quadraticBezier(q0, q1, q2, t)
}
// ===== Easing функции =====
const easing = {
linear: t => t,
easeIn: t => Math.pow(t, 3),
easeOut: t => 1 - Math.pow(1 - t, 3),
easeInOut: t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2,
bounce: t => {
const c4 = (2 * Math.PI) / 3
return t === 0 ? 0 : t === 1 ? 1
: Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1
},
}
// ===== Сравнение: позиция объекта (0 → 100px) =====
console.log('=== Позиция объекта (0→100px) в разные моменты анимации ===')
console.log('t'.padEnd(6), 'linear'.padEnd(10), 'easeIn'.padEnd(10), 'easeOut'.padEnd(10), 'easeInOut')
console.log('-'.repeat(52))
const timePoints = [0, 0.1, 0.25, 0.5, 0.75, 0.9, 1]
for (const t of timePoints) {
const linear = Math.round(easing.linear(t) * 100)
const easeIn = Math.round(easing.easeIn(t) * 100)
const easeOut = Math.round(easing.easeOut(t) * 100)
const easeInOut = Math.round(easing.easeInOut(t) * 100)
console.log(
String(t).padEnd(6),
String(linear).padEnd(10),
String(easeIn).padEnd(10),
String(easeOut).padEnd(10),
easeInOut
)
}
// ===== Визуализация скорости easeOut =====
console.log('\n=== Скорость easeOut (быстро вначале → медленно в конце) ===')
const STEPS = 8
let prev = 0
for (let i = 1; i <= STEPS; i++) {
const t = i / STEPS
const pos = Math.round(easing.easeOut(t) * 100)
const spd = pos - prev
const bar = '█'.repeat(Math.max(0, Math.round(spd / 3)))
console.log(`t=${t.toFixed(2)} pos=${String(pos).padStart(3)} delta=${String(spd).padStart(3)} ${bar}`)
prev = pos
}
// ===== Bounce: значения > 1 (перелёт) =====
console.log('\n=== Bounce анимация (значения > 100px — "перелёт") ===')
for (let i = 0; i <= 10; i++) {
const t = i / 10
const val = Math.round(easing.bounce(t) * 100)
const bar = val > 100
? '█'.repeat(20) + '!'.repeat(Math.round((val - 100) / 3))
: '█'.repeat(Math.round(val / 5))
console.log(`t=${t.toFixed(1)} pos=${String(val).padStart(4)} ${bar}`)
}
// ===== steps() — дискретная анимация =====
console.log('\n=== steps(4) — покадровая анимация (спрайты) ===')
function stepsEasing(n) {
return t => Math.floor(t * n) / n
}
const steps4 = stepsEasing(4)
for (let i = 0; i <= 8; i++) {
const t = i / 8
const pos = Math.round(steps4(t) * 100)
const bar = '█'.repeat(Math.round(pos / 10))
console.log(`t=${t.toFixed(2)} -> ${String(pos).padStart(3)}px ${bar}`)
}
// ===== Применение lerp: интерполяция цвета =====
console.log('\n=== lerp для интерполяции значений ===')
// Анимация opacity: 0 → 1 с easeOut
for (const t of [0, 0.25, 0.5, 0.75, 1]) {
const easedT = easing.easeOut(t)
const opacity = lerp(0, 1, easedT)
const transY = lerp(-20, 0, easedT)
console.log(
`t=${t.toFixed(2)} | opacity=${opacity.toFixed(3)} | translateY=${transY.toFixed(1)}px`
)
}Ты замечал, что одни анимации ощущаются «живыми», а другие — механическими? Карточка, которая «выскакивает» быстро и медленно тормозит — ощущается естественно. Карточка с одинаковой скоростью — роботизированно. Разница — в кривой Безье. В Figma, CSS transitions, After Effects, JavaScript-анимациях — везде используется cubic-bezier.
Анимация — это функция от времени: как быстро меняется значение от начала к концу. Линейная функция даёт одинаковую скорость, что выглядит неестественно. Кривая Безье позволяет задать разгон, торможение, «пружину» — всё что делает интерфейс живым.
animation-timing-functionCSS принимает cubic-bezier(x1, y1, x2, y2) — две контрольные точки кубической кривой Безье. Ось X — время (0 до 1), ось Y — значение свойства:
// Стандартные функции CSS — это конкретные cubic-bezier:
const ease = [0.25, 0.1, 0.25, 1.0] // медленно → быстро → медленно
const easeIn = [0.42, 0.0, 1.0, 1.0] // медленно → быстро
const easeOut = [0.0, 0.0, 0.58, 1.0] // быстро → медленно
const linear = [0.0, 0.0, 1.0, 1.0] // равномерно
const easeInOut = [0.42, 0.0, 0.58, 1.0] // медленно → быстро → медленноКубическая кривая Безье через 4 точки, параметр t ∈ [0, 1]:
// Линейная интерполяция — основа всего
function lerp(a, b, t) {
return a + (b - a) * t
}
// Квадратичная Безье (3 точки)
function quadraticBezier(p0, p1, p2, t) {
return lerp(lerp(p0, p1, t), lerp(p1, p2, t), t)
}
// Кубическая Безье (4 точки) — используется в CSS
function cubicBezier(p0, p1, p2, p3, t) {
const q0 = lerp(p0, p1, t)
const q1 = lerp(p1, p2, t)
const q2 = lerp(p2, p3, t)
return quadraticBezier(q0, q1, q2, t)
}В коде анимаций удобнее использовать простые формулы без вычисления CSS cubic-bezier:
const easing = {
linear: t => t,
easeOut: t => 1 - Math.pow(1 - t, 3), // быстро в начале
easeIn: t => Math.pow(t, 3), // медленно в начале
easeInOut: t => t < 0.5
? 4 * t * t * t
: 1 - Math.pow(-2 * t + 2, 3) / 2,
// "Пружина" с перебросом:
bounce: t => {
const c4 = (2 * Math.PI) / 3
return t === 0 ? 0 : t === 1 ? 1
: Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1
}
}steps(n) делит анимацию на N дискретных шагов. Используется для спрайт-анимаций:
function stepsEasing(numSteps) {
return t => Math.floor(t * numSteps) / numSteps
}
// steps(4): t=0.3 → 0.25 (прыжок), t=0.6 → 0.5 (прыжок)
// Нет плавности — только дискретные позицииОшибка 1: Линейная анимация для UI-элементов
// ПЛОХО — роботизированно, неестественно
element.style.transition = 'transform 0.3s linear'
// ХОРОШО — естественное движение
element.style.transition = 'transform 0.3s ease-out'Ошибка 2: Слишком долгие анимации
// Человек ждёт после 200ms — анимация мешает работе
// ПЛОХО: 0.8s для появления карточки
// ХОРОШО: 0.15-0.3s для появления, 0.1-0.2s для исчезновенияОшибка 3: Анимировать layout-свойства вместо transform
// ПЛОХО — вызывает reflow на каждый кадр
element.style.left = lerp(0, 200, t) + 'px'
// ХОРОШО — GPU, без reflow
element.style.transform = `translateX(${lerp(0, 200, t)}px)`d3.easing.cubicOut и др.Математика кривой Безье: lerp, easing-функции, сравнение анимаций, steps()
// Математика кривой Безье — чистый JS, без DOM
// ===== Базовые функции =====
function lerp(a, b, t) {
return a + (b - a) * t
}
function quadraticBezier(p0, p1, p2, t) {
return lerp(lerp(p0, p1, t), lerp(p1, p2, t), t)
}
function cubicBezierPoint(p0, p1, p2, p3, t) {
const q0 = lerp(p0, p1, t)
const q1 = lerp(p1, p2, t)
const q2 = lerp(p2, p3, t)
return quadraticBezier(q0, q1, q2, t)
}
// ===== Easing функции =====
const easing = {
linear: t => t,
easeIn: t => Math.pow(t, 3),
easeOut: t => 1 - Math.pow(1 - t, 3),
easeInOut: t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2,
bounce: t => {
const c4 = (2 * Math.PI) / 3
return t === 0 ? 0 : t === 1 ? 1
: Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1
},
}
// ===== Сравнение: позиция объекта (0 → 100px) =====
console.log('=== Позиция объекта (0→100px) в разные моменты анимации ===')
console.log('t'.padEnd(6), 'linear'.padEnd(10), 'easeIn'.padEnd(10), 'easeOut'.padEnd(10), 'easeInOut')
console.log('-'.repeat(52))
const timePoints = [0, 0.1, 0.25, 0.5, 0.75, 0.9, 1]
for (const t of timePoints) {
const linear = Math.round(easing.linear(t) * 100)
const easeIn = Math.round(easing.easeIn(t) * 100)
const easeOut = Math.round(easing.easeOut(t) * 100)
const easeInOut = Math.round(easing.easeInOut(t) * 100)
console.log(
String(t).padEnd(6),
String(linear).padEnd(10),
String(easeIn).padEnd(10),
String(easeOut).padEnd(10),
easeInOut
)
}
// ===== Визуализация скорости easeOut =====
console.log('\n=== Скорость easeOut (быстро вначале → медленно в конце) ===')
const STEPS = 8
let prev = 0
for (let i = 1; i <= STEPS; i++) {
const t = i / STEPS
const pos = Math.round(easing.easeOut(t) * 100)
const spd = pos - prev
const bar = '█'.repeat(Math.max(0, Math.round(spd / 3)))
console.log(`t=${t.toFixed(2)} pos=${String(pos).padStart(3)} delta=${String(spd).padStart(3)} ${bar}`)
prev = pos
}
// ===== Bounce: значения > 1 (перелёт) =====
console.log('\n=== Bounce анимация (значения > 100px — "перелёт") ===')
for (let i = 0; i <= 10; i++) {
const t = i / 10
const val = Math.round(easing.bounce(t) * 100)
const bar = val > 100
? '█'.repeat(20) + '!'.repeat(Math.round((val - 100) / 3))
: '█'.repeat(Math.round(val / 5))
console.log(`t=${t.toFixed(1)} pos=${String(val).padStart(4)} ${bar}`)
}
// ===== steps() — дискретная анимация =====
console.log('\n=== steps(4) — покадровая анимация (спрайты) ===')
function stepsEasing(n) {
return t => Math.floor(t * n) / n
}
const steps4 = stepsEasing(4)
for (let i = 0; i <= 8; i++) {
const t = i / 8
const pos = Math.round(steps4(t) * 100)
const bar = '█'.repeat(Math.round(pos / 10))
console.log(`t=${t.toFixed(2)} -> ${String(pos).padStart(3)}px ${bar}`)
}
// ===== Применение lerp: интерполяция цвета =====
console.log('\n=== lerp для интерполяции значений ===')
// Анимация opacity: 0 → 1 с easeOut
for (const t of [0, 0.25, 0.5, 0.75, 1]) {
const easedT = easing.easeOut(t)
const opacity = lerp(0, 1, easedT)
const transY = lerp(-20, 0, easedT)
console.log(
`t=${t.toFixed(2)} | opacity=${opacity.toFixed(3)} | translateY=${transY.toFixed(1)}px`
)
}Реализуй набор инструментов для анимации. Реализуй: - `lerp(a, b, t)` — линейная интерполяция - `easeOutCubic(t)` — функция плавности: быстро в начале, медленно в конце (`1 - (1-t)^3`) - `easeInCubic(t)` — медленно в начале, быстро в конце (`t^3`) - `animatePositions(from, to, steps)` — возвращает массив из `(steps+1)` позиций с easeOut
lerp: a + (b - a) * t. easeOutCubic: 1 - Math.pow(1 - t, 3). easeInCubic: Math.pow(t, 3). animatePositions: цикл i от 0 до steps, t = i/steps, easedT = easeOutCubic(t), push(Math.round(lerp(from, to, easedT)))