Карточка товара появляется с fade-in при загрузке страницы. Кнопка плавно меняет цвет при наведении. Спиннер крутится пока грузятся данные. Уведомление «въезжает» сверху. Все эти эффекты — CSS transitions и animations. Они работают на GPU, не блокируют JavaScript, и браузер оптимизирует их автоматически.
JavaScript-анимации через setInterval или setTimeout — дорогие (работают на CPU), мигают при высокой нагрузке, и продолжают работать когда вкладка не активна. CSS animations — декларативны, браузер оптимизирует их сам, и они мгновенно останавливаются на фоновых вкладках.
animation-timing-function: cubic-bezier() — это те же функцииelement.style.transition, element.classList.addTransition запускается когда 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 — это дорого!
// ВСЕГДА перечисляй конкретные свойства.@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-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, не вызывают reflow:
// transform: translate, rotate, scale
// opacity
// ПЛОХО — вызывают reflow (пересчёт layout):
// width, height, top, left, margin, padding, border
// Правило: если можно сделать через transform/opacity — делай так// Переносит элемент на отдельный GPU-слой заранее
// element.style.willChange = 'transform'
// Устанавливай перед анимацией, убирай после:
el.style.willChange = 'transform'
el.classList.add('animate')
el.addEventListener('transitionend', () => {
el.style.willChange = 'auto'
}, { once: true })
// НЕ злоупотребляй — каждый слой потребляет память GPU// Современный способ управлять 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; }animation-fill-mode: forwardsСимуляция логики 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(', '))Карточка товара появляется с fade-in при загрузке страницы. Кнопка плавно меняет цвет при наведении. Спиннер крутится пока грузятся данные. Уведомление «въезжает» сверху. Все эти эффекты — CSS transitions и animations. Они работают на GPU, не блокируют JavaScript, и браузер оптимизирует их автоматически.
JavaScript-анимации через setInterval или setTimeout — дорогие (работают на CPU), мигают при высокой нагрузке, и продолжают работать когда вкладка не активна. CSS animations — декларативны, браузер оптимизирует их сам, и они мгновенно останавливаются на фоновых вкладках.
animation-timing-function: cubic-bezier() — это те же функцииelement.style.transition, element.classList.addTransition запускается когда 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 — это дорого!
// ВСЕГДА перечисляй конкретные свойства.@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-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, не вызывают reflow:
// transform: translate, rotate, scale
// opacity
// ПЛОХО — вызывают reflow (пересчёт layout):
// width, height, top, left, margin, padding, border
// Правило: если можно сделать через transform/opacity — делай так// Переносит элемент на отдельный GPU-слой заранее
// element.style.willChange = 'transform'
// Устанавливай перед анимацией, убирай после:
el.style.willChange = 'transform'
el.classList.add('animate')
el.addEventListener('transitionend', () => {
el.style.willChange = 'auto'
}, { once: true })
// НЕ злоупотребляй — каждый слой потребляет память GPU// Современный способ управлять 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; }animation-fill-mode: forwardsСимуляция логики 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)