← JavaScript/Кривые Безье#164 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

Кривые Безье

Ты замечал, что одни анимации ощущаются «живыми», а другие — механическими? Карточка, которая «выскакивает» быстро и медленно тормозит — ощущается естественно. Карточка с одинаковой скоростью — роботизированно. Разница — в кривой Безье. В Figma, CSS transitions, After Effects, JavaScript-анимациях — везде используется cubic-bezier.

Какую проблему решает

Анимация — это функция от времени: как быстро меняется значение от начала к концу. Линейная функция даёт одинаковую скорость, что выглядит неестественно. Кривая Безье позволяет задать разгон, торможение, «пружину» — всё что делает интерфейс живым.

На основе предыдущих уроков

  • Math: Math.pow, Math.PI используются в формулах easing
  • Функции: функции плавности — это функции высшего порядка
  • CSS анимации: cubic-bezier используется в animation-timing-function
  • Как работает cubic-bezier

    CSS принимает 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() — покадровая анимация

    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 (прыжок)
    // Нет плавности — только дискретные позиции

    UX-рекомендации

  • ease-out → появление элементов (быстрый старт, мягкая остановка = «приземление»)
  • ease-in → исчезновение элементов (плавный уход, ускорение = «улетает»)
  • ease-in-out → перемещение между точками (симметрично)
  • linear → бесконечное вращение, прогресс-бары
  • spring/bounce → интерактивные элементы (кнопки, уведомления)
  • Типичные ошибки

    Ошибка 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)`

    В реальных проектах

  • React Spring / Framer Motion: библиотеки физических анимаций на основе тех же формул
  • D3.js: визуализация данных использует d3.easing.cubicOut и др.
  • Game dev: lerp для плавного следования камеры за персонажем
  • Scroll анимации: Intersection Observer + easing для плавного появления элементов
  • Примеры

    Математика кривой Безье: 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.

    Какую проблему решает

    Анимация — это функция от времени: как быстро меняется значение от начала к концу. Линейная функция даёт одинаковую скорость, что выглядит неестественно. Кривая Безье позволяет задать разгон, торможение, «пружину» — всё что делает интерфейс живым.

    На основе предыдущих уроков

  • Math: Math.pow, Math.PI используются в формулах easing
  • Функции: функции плавности — это функции высшего порядка
  • CSS анимации: cubic-bezier используется в animation-timing-function
  • Как работает cubic-bezier

    CSS принимает 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() — покадровая анимация

    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 (прыжок)
    // Нет плавности — только дискретные позиции

    UX-рекомендации

  • ease-out → появление элементов (быстрый старт, мягкая остановка = «приземление»)
  • ease-in → исчезновение элементов (плавный уход, ускорение = «улетает»)
  • ease-in-out → перемещение между точками (симметрично)
  • linear → бесконечное вращение, прогресс-бары
  • spring/bounce → интерактивные элементы (кнопки, уведомления)
  • Типичные ошибки

    Ошибка 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)`

    В реальных проектах

  • React Spring / Framer Motion: библиотеки физических анимаций на основе тех же формул
  • D3.js: визуализация данных использует d3.easing.cubicOut и др.
  • Game dev: lerp для плавного следования камеры за персонажем
  • Scroll анимации: Intersection Observer + easing для плавного появления элементов
  • Примеры

    Математика кривой Безье: 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)))

    Загружаем среду выполнения...
    Загружаем AI-помощника...