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

CSS анимации

Карточка товара появляется с fade-in при загрузке страницы. Кнопка плавно меняет цвет при наведении. Спиннер крутится пока грузятся данные. Уведомление «въезжает» сверху. Все эти эффекты — CSS transitions и animations. Они работают на GPU, не блокируют JavaScript, и браузер оптимизирует их автоматически.

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

JavaScript-анимации через setInterval или setTimeout — дорогие (работают на CPU), мигают при высокой нагрузке, и продолжают работать когда вкладка не активна. CSS animations — декларативны, браузер оптимизирует их сам, и они мгновенно останавливаются на фоновых вкладках.

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

  • Кривые Безье: animation-timing-function: cubic-bezier() — это те же функции
  • requestAnimationFrame: когда CSS недостаточно и нужен JS
  • CSS свойства из JS: element.style.transition, element.classList.add
  • Transitions — переходы между состояниями

    Transition запускается когда CSS-свойство меняется (ховер, класс, JS):

    // transition: property duration timing-function delay;
    // transition: opacity 0.3s ease-out 0s;
    
    // Несколько свойств:
    // transition: opacity 0.3s ease-out, transform 0.5s cubic-bezier(0.25, 0.1, 0.25, 1);
    
    // НЕ делай transition: all — это дорого!
    // ВСЕГДА перечисляй конкретные свойства.

    Animations — @keyframes для сложных анимаций

    @keyframes fade-in {
      from { opacity: 0; transform: translateY(-10px); }
      to   { opacity: 1; transform: translateY(0); }
    }
    
    @keyframes bounce {
      0%   { transform: translateY(0); }
      50%  { transform: translateY(-30px); }
      70%  { transform: translateY(-15px); }
      100% { transform: translateY(0); }
    }
    
    .element {
      animation: fade-in 0.4s ease-out forwards;
    }

    Ключевые свойства animation

    // animation-fill-mode:
    // forwards  — сохраняет состояние последнего кадра после окончания
    // backwards — применяет состояние первого кадра ВО ВРЕМЯ задержки (delay)
    // both      — forwards + backwards
    
    // animation-iteration-count:
    // infinite  — бесконечно
    // 3         — три раза
    
    // animation-direction:
    // alternate       — вперёд → назад → вперёд...
    // alternate-reverse — назад → вперёд...
    // reverse         — всегда в обратном порядке
    
    // animation-play-state:
    // paused / running — можно менять из JS!

    GPU-ускоренные свойства — только они для анимации

    // ХОРОШО — GPU, не вызывают reflow:
    // transform: translate, rotate, scale
    // opacity
    
    // ПЛОХО — вызывают reflow (пересчёт layout):
    // width, height, top, left, margin, padding, border
    
    // Правило: если можно сделать через transform/opacity — делай так

    will-change — оптимизация

    // Переносит элемент на отдельный GPU-слой заранее
    // element.style.willChange = 'transform'
    
    // Устанавливай перед анимацией, убирай после:
    el.style.willChange = 'transform'
    el.classList.add('animate')
    el.addEventListener('transitionend', () => {
      el.style.willChange = 'auto'
    }, { once: true })
    
    // НЕ злоупотребляй — каждый слой потребляет память GPU

    Web Animations API — анимации из JS

    // Современный способ управлять CSS-анимациями из JS:
    const anim = element.animate(
      [
        { opacity: 0, transform: 'translateY(-10px)' },
        { opacity: 1, transform: 'translateY(0)' }
      ],
      { duration: 400, easing: 'ease-out', fill: 'forwards' }
    )
    
    anim.pause()         // поставить на паузу
    anim.play()          // возобновить
    anim.cancel()        // отменить
    await anim.finished  // Promise — дождаться конца

    Типичные ошибки

    Ошибка 1: Забыть fill-mode forwards

    /* ПЛОХО — элемент сбросится в исходное состояние */
    .fade-in { animation: fade-in 0.4s ease-out; }
    
    /* ХОРОШО — сохраняет конечное состояние */
    .fade-in { animation: fade-in 0.4s ease-out forwards; }

    Ошибка 2: Анимировать свойства, вызывающие reflow

    /* ПЛОХО — reflow каждый кадр, 60 раз в секунду */
    @keyframes move { from { left: 0; } to { left: 200px; } }
    
    /* ХОРОШО — GPU, нет reflow */
    @keyframes move { from { transform: translateX(0); } to { transform: translateX(200px); } }

    Ошибка 3: transition: all

    /* ПЛОХО — браузер проверяет ВСЕ свойства */
    .btn { transition: all 0.3s; }
    
    /* ХОРОШО — только нужные */
    .btn { transition: background-color 0.2s, transform 0.15s; }

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

  • Loading states: спиннеры, skeleton screens через CSS animations
  • Micro-interactions: кнопки, hover-эффекты, focus-стили
  • Page transitions: fade между маршрутами в React Router
  • Toast-уведомления: slide-in/slide-out с animation-fill-mode: forwards
  • Scroll-анимации: элементы появляются при прокрутке (Intersection Observer + класс с animation)
  • Примеры

    Симуляция логики CSS анимаций: интерполяция значений, easing, цветовые переходы

    // Симуляция логики CSS анимаций — чистый JS, без DOM
    // Демонстрируем как браузер вычисляет промежуточные кадры
    
    // Функции плавности (имитация CSS timing functions)
    const easingFns = {
      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,
    }
    
    // Интерполяция числа
    function interpolateNumber(from, to, t) {
      return from + (to - from) * t
    }
    
    // Интерполяция HEX цвета
    function hexToRgb(hex) {
      const n = parseInt(hex.replace('#', ''), 16)
      return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 }
    }
    
    function rgbToHex(r, g, b) {
      return '#' + [r, g, b].map(v => Math.round(v).toString(16).padStart(2, '0')).join('')
    }
    
    function interpolateColor(fromHex, toHex, t) {
      const f = hexToRgb(fromHex), to = hexToRgb(toHex)
      return rgbToHex(
        interpolateNumber(f.r, to.r, t),
        interpolateNumber(f.g, to.g, t),
        interpolateNumber(f.b, to.b, t)
      )
    }
    
    // Симулятор анимации: вычисляет ключевые кадры
    function simulateAnimation(keyframes, durationMs, easingName = 'easeOut', numPoints = 6) {
      const fn = easingFns[easingName]
      const results = []
    
      for (let i = 0; i <= numPoints; i++) {
        const rawT = i / numPoints
        const t    = fn(rawT)
        const frame = {
          time:   Math.round(rawT * durationMs),
          t:      rawT,
          easedT: Math.round(t * 1000) / 1000,
        }
    
        for (const [key, [from, to]] of Object.entries(keyframes)) {
          if (key === 'color' || key === 'background') {
            frame[key] = interpolateColor(from, to, t)
          } else {
            frame[key] = Math.round(interpolateNumber(from, to, t) * 100) / 100
          }
        }
        results.push(frame)
      }
      return results
    }
    
    // === fade-in анимация ===
    console.log('=== fade-in: opacity 0→1, translateY -20→0px (easeOut) ===')
    const fadeFrames = simulateAnimation(
      { opacity: [0, 1], translateY: [-20, 0] },
      400, 'easeOut', 5
    )
    console.log('time(ms)  t      easedT  opacity  translateY')
    console.log('-'.repeat(52))
    for (const f of fadeFrames) {
      console.log(
        String(f.time).padEnd(10) +
        String(f.t.toFixed(2)).padEnd(7) +
        String(f.easedT).padEnd(8) +
        String(f.opacity).padEnd(9) +
        f.translateY + 'px'
      )
    }
    
    // === hover: цвет кнопки ===
    console.log('\n=== Hover: цвет кнопки #3498db → #2980b9 (easeOut, 200ms) ===')
    const colorFrames = simulateAnimation(
      { background: ['#3498db', '#2980b9'] },
      200, 'easeOut', 4
    )
    for (const f of colorFrames) {
      console.log(`t=${f.t.toFixed(2)} (${f.time}ms) → ${f.background}`)
    }
    
    // === fill-mode: forwards ===
    console.log('\n=== animation-fill-mode ===')
    console.log('forwards:  элемент остаётся в состоянии последнего кадра (100%)')
    console.log('backwards: состояние первого кадра применяется ВО ВРЕМЯ delay')
    console.log('both:      forwards + backwards')
    console.log()
    console.log('Без forwards: fade-in 0→1, потом opacity вернётся к 0')
    console.log('С forwards:   fade-in 0→1, opacity остаётся 1')
    
    // === Сравнение easing при t=0.25 ===
    console.log('\n=== Сравнение easing: прогресс при t=0.25 (четверть пути) ===')
    for (const [name, fn] of Object.entries(easingFns)) {
      const pos = Math.round(fn(0.25) * 100)
      const bar = '▓'.repeat(Math.round(pos / 4))
      console.log(`${name.padEnd(12)}: ${String(pos).padStart(3)}%  ${bar}`)
    }
    
    // === GPU vs CPU свойства ===
    console.log('\n=== GPU vs CPU свойства для анимации ===')
    const gpuProps  = ['transform', 'opacity']
    const cpuProps  = ['width', 'height', 'top', 'left', 'margin', 'padding']
    console.log('GPU (animate freely):', gpuProps.join(', '))
    console.log('CPU/reflow (избегать):', cpuProps.join(', '))

    CSS анимации

    Карточка товара появляется с fade-in при загрузке страницы. Кнопка плавно меняет цвет при наведении. Спиннер крутится пока грузятся данные. Уведомление «въезжает» сверху. Все эти эффекты — CSS transitions и animations. Они работают на GPU, не блокируют JavaScript, и браузер оптимизирует их автоматически.

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

    JavaScript-анимации через setInterval или setTimeout — дорогие (работают на CPU), мигают при высокой нагрузке, и продолжают работать когда вкладка не активна. CSS animations — декларативны, браузер оптимизирует их сам, и они мгновенно останавливаются на фоновых вкладках.

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

  • Кривые Безье: animation-timing-function: cubic-bezier() — это те же функции
  • requestAnimationFrame: когда CSS недостаточно и нужен JS
  • CSS свойства из JS: element.style.transition, element.classList.add
  • Transitions — переходы между состояниями

    Transition запускается когда CSS-свойство меняется (ховер, класс, JS):

    // transition: property duration timing-function delay;
    // transition: opacity 0.3s ease-out 0s;
    
    // Несколько свойств:
    // transition: opacity 0.3s ease-out, transform 0.5s cubic-bezier(0.25, 0.1, 0.25, 1);
    
    // НЕ делай transition: all — это дорого!
    // ВСЕГДА перечисляй конкретные свойства.

    Animations — @keyframes для сложных анимаций

    @keyframes fade-in {
      from { opacity: 0; transform: translateY(-10px); }
      to   { opacity: 1; transform: translateY(0); }
    }
    
    @keyframes bounce {
      0%   { transform: translateY(0); }
      50%  { transform: translateY(-30px); }
      70%  { transform: translateY(-15px); }
      100% { transform: translateY(0); }
    }
    
    .element {
      animation: fade-in 0.4s ease-out forwards;
    }

    Ключевые свойства animation

    // animation-fill-mode:
    // forwards  — сохраняет состояние последнего кадра после окончания
    // backwards — применяет состояние первого кадра ВО ВРЕМЯ задержки (delay)
    // both      — forwards + backwards
    
    // animation-iteration-count:
    // infinite  — бесконечно
    // 3         — три раза
    
    // animation-direction:
    // alternate       — вперёд → назад → вперёд...
    // alternate-reverse — назад → вперёд...
    // reverse         — всегда в обратном порядке
    
    // animation-play-state:
    // paused / running — можно менять из JS!

    GPU-ускоренные свойства — только они для анимации

    // ХОРОШО — GPU, не вызывают reflow:
    // transform: translate, rotate, scale
    // opacity
    
    // ПЛОХО — вызывают reflow (пересчёт layout):
    // width, height, top, left, margin, padding, border
    
    // Правило: если можно сделать через transform/opacity — делай так

    will-change — оптимизация

    // Переносит элемент на отдельный GPU-слой заранее
    // element.style.willChange = 'transform'
    
    // Устанавливай перед анимацией, убирай после:
    el.style.willChange = 'transform'
    el.classList.add('animate')
    el.addEventListener('transitionend', () => {
      el.style.willChange = 'auto'
    }, { once: true })
    
    // НЕ злоупотребляй — каждый слой потребляет память GPU

    Web Animations API — анимации из JS

    // Современный способ управлять CSS-анимациями из JS:
    const anim = element.animate(
      [
        { opacity: 0, transform: 'translateY(-10px)' },
        { opacity: 1, transform: 'translateY(0)' }
      ],
      { duration: 400, easing: 'ease-out', fill: 'forwards' }
    )
    
    anim.pause()         // поставить на паузу
    anim.play()          // возобновить
    anim.cancel()        // отменить
    await anim.finished  // Promise — дождаться конца

    Типичные ошибки

    Ошибка 1: Забыть fill-mode forwards

    /* ПЛОХО — элемент сбросится в исходное состояние */
    .fade-in { animation: fade-in 0.4s ease-out; }
    
    /* ХОРОШО — сохраняет конечное состояние */
    .fade-in { animation: fade-in 0.4s ease-out forwards; }

    Ошибка 2: Анимировать свойства, вызывающие reflow

    /* ПЛОХО — reflow каждый кадр, 60 раз в секунду */
    @keyframes move { from { left: 0; } to { left: 200px; } }
    
    /* ХОРОШО — GPU, нет reflow */
    @keyframes move { from { transform: translateX(0); } to { transform: translateX(200px); } }

    Ошибка 3: transition: all

    /* ПЛОХО — браузер проверяет ВСЕ свойства */
    .btn { transition: all 0.3s; }
    
    /* ХОРОШО — только нужные */
    .btn { transition: background-color 0.2s, transform 0.15s; }

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

  • Loading states: спиннеры, skeleton screens через CSS animations
  • Micro-interactions: кнопки, hover-эффекты, focus-стили
  • Page transitions: fade между маршрутами в React Router
  • Toast-уведомления: slide-in/slide-out с animation-fill-mode: forwards
  • Scroll-анимации: элементы появляются при прокрутке (Intersection Observer + класс с animation)
  • Примеры

    Симуляция логики CSS анимаций: интерполяция значений, easing, цветовые переходы

    // Симуляция логики CSS анимаций — чистый JS, без DOM
    // Демонстрируем как браузер вычисляет промежуточные кадры
    
    // Функции плавности (имитация CSS timing functions)
    const easingFns = {
      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,
    }
    
    // Интерполяция числа
    function interpolateNumber(from, to, t) {
      return from + (to - from) * t
    }
    
    // Интерполяция HEX цвета
    function hexToRgb(hex) {
      const n = parseInt(hex.replace('#', ''), 16)
      return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 }
    }
    
    function rgbToHex(r, g, b) {
      return '#' + [r, g, b].map(v => Math.round(v).toString(16).padStart(2, '0')).join('')
    }
    
    function interpolateColor(fromHex, toHex, t) {
      const f = hexToRgb(fromHex), to = hexToRgb(toHex)
      return rgbToHex(
        interpolateNumber(f.r, to.r, t),
        interpolateNumber(f.g, to.g, t),
        interpolateNumber(f.b, to.b, t)
      )
    }
    
    // Симулятор анимации: вычисляет ключевые кадры
    function simulateAnimation(keyframes, durationMs, easingName = 'easeOut', numPoints = 6) {
      const fn = easingFns[easingName]
      const results = []
    
      for (let i = 0; i <= numPoints; i++) {
        const rawT = i / numPoints
        const t    = fn(rawT)
        const frame = {
          time:   Math.round(rawT * durationMs),
          t:      rawT,
          easedT: Math.round(t * 1000) / 1000,
        }
    
        for (const [key, [from, to]] of Object.entries(keyframes)) {
          if (key === 'color' || key === 'background') {
            frame[key] = interpolateColor(from, to, t)
          } else {
            frame[key] = Math.round(interpolateNumber(from, to, t) * 100) / 100
          }
        }
        results.push(frame)
      }
      return results
    }
    
    // === fade-in анимация ===
    console.log('=== fade-in: opacity 0→1, translateY -20→0px (easeOut) ===')
    const fadeFrames = simulateAnimation(
      { opacity: [0, 1], translateY: [-20, 0] },
      400, 'easeOut', 5
    )
    console.log('time(ms)  t      easedT  opacity  translateY')
    console.log('-'.repeat(52))
    for (const f of fadeFrames) {
      console.log(
        String(f.time).padEnd(10) +
        String(f.t.toFixed(2)).padEnd(7) +
        String(f.easedT).padEnd(8) +
        String(f.opacity).padEnd(9) +
        f.translateY + 'px'
      )
    }
    
    // === hover: цвет кнопки ===
    console.log('\n=== Hover: цвет кнопки #3498db → #2980b9 (easeOut, 200ms) ===')
    const colorFrames = simulateAnimation(
      { background: ['#3498db', '#2980b9'] },
      200, 'easeOut', 4
    )
    for (const f of colorFrames) {
      console.log(`t=${f.t.toFixed(2)} (${f.time}ms) → ${f.background}`)
    }
    
    // === fill-mode: forwards ===
    console.log('\n=== animation-fill-mode ===')
    console.log('forwards:  элемент остаётся в состоянии последнего кадра (100%)')
    console.log('backwards: состояние первого кадра применяется ВО ВРЕМЯ delay')
    console.log('both:      forwards + backwards')
    console.log()
    console.log('Без forwards: fade-in 0→1, потом opacity вернётся к 0')
    console.log('С forwards:   fade-in 0→1, opacity остаётся 1')
    
    // === Сравнение easing при t=0.25 ===
    console.log('\n=== Сравнение easing: прогресс при t=0.25 (четверть пути) ===')
    for (const [name, fn] of Object.entries(easingFns)) {
      const pos = Math.round(fn(0.25) * 100)
      const bar = '▓'.repeat(Math.round(pos / 4))
      console.log(`${name.padEnd(12)}: ${String(pos).padStart(3)}%  ${bar}`)
    }
    
    // === GPU vs CPU свойства ===
    console.log('\n=== GPU vs CPU свойства для анимации ===')
    const gpuProps  = ['transform', 'opacity']
    const cpuProps  = ['width', 'height', 'top', 'left', 'margin', 'padding']
    console.log('GPU (animate freely):', gpuProps.join(', '))
    console.log('CPU/reflow (избегать):', cpuProps.join(', '))

    Задание

    Реализуй функцию интерполяции цвета и генератор цветового перехода для анимаций. Реализуй: - `interpolateCSSColor(fromHex, toHex, t)` — интерполирует между двумя HEX-цветами при прогрессе t (0..1). Возвращает HEX-строку - `generateColorTransition(fromHex, toHex, steps)` — возвращает массив из `(steps+1)` промежуточных цветов - `easeOutTransition(fromHex, toHex, steps)` — то же самое, но с применением `easeOut` к прогрессу

    Подсказка

    interpolateCSSColor: hexToRgb на оба цвета, r = from.r + (to.r - from.r) * t, аналогично g и b, rgbToHex(r,g,b). generateColorTransition: цикл i от 0 до steps, t = i/steps. easeOutTransition: easedT = 1 - Math.pow(1-t, 3)

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