深入学习Canvas缓动动画技术,掌握各种缓动函数的原理和应用。缓动动画是让动画更自然、更有表现力的关键技术。通过缓动函数控制动画的速度变化,可以创造出各种生动的效果。
缓动(Easing)是指在动画过程中,物体运动速度的变化规律。
| 类型 | 特点 | 效果 |
|---|---|---|
| 匀速 | 速度恒定 | 机械、生硬 |
| 缓动 | 速度变化 | 自然、生动 |
// 匀速动画
function linear(t) {
return t
}
// 缓动动画(缓出)
function easeOut(t) {
return t * (2 - t)
}
缓动函数接收一个时间进度(0-1),返回位置进度(0-1)。
// t: 当前时间(0-1)
// b: 起始值
// c: 变化量
// d: 总时长
function easeInOutQuad(t, b, c, d) {
t /= d / 2
if (t < 1) return c / 2 * t * t + b
t--
return -c / 2 * (t * (t - 2) - 1) + b
}
const quad = {
easeIn: t => t * t,
easeOut: t => t * (2 - t),
easeInOut: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
}
const cubic = {
easeIn: t => t * t * t,
easeOut: t => (--t) * t * t + 1,
easeInOut: t => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
}
const quart = {
easeIn: t => t * t * t * t,
easeOut: t => 1 - (--t) * t * t * t,
easeInOut: t => t < 0.5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t
}
const quint = {
easeIn: t => t * t * t * t * t,
easeOut: t => 1 + (--t) * t * t * t * t,
easeInOut: t => t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t
}
const sine = {
easeIn: t => 1 - Math.cos(t * Math.PI / 2),
easeOut: t => Math.sin(t * Math.PI / 2),
easeInOut: t => -(Math.cos(Math.PI * t) - 1) / 2
}
const expo = {
easeIn: t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1)),
easeOut: t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t),
easeInOut: t => {
if (t === 0) return 0
if (t === 1) return 1
if (t < 0.5) return Math.pow(2, 20 * t - 10) / 2
return (2 - Math.pow(2, -20 * t + 10)) / 2
}
}
const circ = {
easeIn: t => 1 - Math.sqrt(1 - t * t),
easeOut: t => Math.sqrt(1 - (--t) * t),
easeInOut: t => t < 0.5
? (1 - Math.sqrt(1 - 4 * t * t)) / 2
: (Math.sqrt(1 - Math.pow(-2 * t + 2, 2)) + 1) / 2
}
const elastic = {
easeIn: t => {
if (t === 0) return 0
if (t === 1) return 1
return -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * (2 * Math.PI) / 3)
},
easeOut: t => {
if (t === 0) return 0
if (t === 1) return 1
return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * (2 * Math.PI) / 3) + 1
},
easeInOut: t => {
if (t === 0) return 0
if (t === 1) return 1
if (t < 0.5) {
return -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * (2 * Math.PI) / 4.5)) / 2
}
return (Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * (2 * Math.PI) / 4.5)) / 2 + 1
}
}
const back = {
easeIn: t => {
const c1 = 1.70158
const c3 = c1 + 1
return c3 * t * t * t - c1 * t * t
},
easeOut: t => {
const c1 = 1.70158
const c3 = c1 + 1
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2)
},
easeInOut: t => {
const c1 = 1.70158
const c2 = c1 * 1.525
return t < 0.5
? (Math.pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
: (Math.pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2
}
}
const bounce = {
easeIn: t => 1 - bounce.easeOut(1 - t),
easeOut: t => {
const n1 = 7.5625
const d1 = 2.75
if (t < 1 / d1) {
return n1 * t * t
} else if (t < 2 / d1) {
return n1 * (t -= 1.5 / d1) * t + 0.75
} else if (t < 2.5 / d1) {
return n1 * (t -= 2.25 / d1) * t + 0.9375
} else {
return n1 * (t -= 2.625 / d1) * t + 0.984375
}
},
easeInOut: t => t < 0.5
? (1 - bounce.easeOut(1 - 2 * t)) / 2
: (1 + bounce.easeOut(2 * t - 1)) / 2
}
class Tween {
constructor(options) {
this.target = options.target
this.property = options.property
this.from = options.from
this.to = options.to
this.duration = options.duration || 1000
this.easing = options.easing || (t => t)
this.onUpdate = options.onUpdate
this.onComplete = options.onComplete
this.startTime = null
this.running = false
}
start() {
this.startTime = performance.now()
this.running = true
this.tick()
return this
}
tick() {
if (!this.running) return
const elapsed = performance.now() - this.startTime
const progress = Math.min(elapsed / this.duration, 1)
const easedProgress = this.easing(progress)
const value = this.from + (this.to - this.from) * easedProgress
this.target[this.property] = value
if (this.onUpdate) {
this.onUpdate(value, progress)
}
if (progress < 1) {
requestAnimationFrame(() => this.tick())
} else {
this.running = false
if (this.onComplete) {
this.onComplete()
}
}
}
stop() {
this.running = false
return this
}
}
const ball = { x: 50 }
new Tween({
target: ball,
property: 'x',
from: 50,
to: 350,
duration: 1000,
easing: easings.easeOutBounce
}).start()
class MultiTween {
constructor(options) {
this.target = options.target
this.properties = options.properties
this.duration = options.duration || 1000
this.easing = options.easing || (t => t)
this.onUpdate = options.onUpdate
this.onComplete = options.onComplete
this.startTime = null
this.running = false
}
start() {
this.startTime = performance.now()
this.running = true
this.tick()
return this
}
tick() {
if (!this.running) return
const elapsed = performance.now() - this.startTime
const progress = Math.min(elapsed / this.duration, 1)
const easedProgress = this.easing(progress)
Object.entries(this.properties).forEach(([prop, values]) => {
this.target[prop] = values.from + (values.to - values.from) * easedProgress
})
if (this.onUpdate) {
this.onUpdate(this.target, progress)
}
if (progress < 1) {
requestAnimationFrame(() => this.tick())
} else {
this.running = false
if (this.onComplete) {
this.onComplete()
}
}
}
}
new MultiTween({
target: element.style,
properties: {
left: { from: 0, to: 300 },
top: { from: 0, to: 200 },
opacity: { from: 1, to: 0.5 }
},
duration: 800,
easing: easings.easeOutCubic
}).start()
| 场景 | 推荐缓动 | 原因 |
|---|---|---|
| UI元素出现 | easeOut | 自然进入 |
| UI元素消失 | easeIn | 自然退出 |
| 弹出框 | easeOutBack | 有弹性感 |
| 按钮点击 | easeOutQuad | 快速响应 |
| 页面滚动 | easeOutCubic | 平滑停止 |
| 加载动画 | easeInOut | 循环流畅 |
// easeIn:开始慢,结束快
// 适合:元素离开屏幕
// easeOut:开始快,结束慢
// 适合:元素进入屏幕
// easeInOut:开始慢,中间快,结束慢
// 适合:循环动画、页面滚动
const EasingLibrary = {
linear: t => t,
quad: {
in: t => t * t,
out: t => t * (2 - t),
inOut: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
},
cubic: {
in: t => t * t * t,
out: t => (--t) * t * t + 1,
inOut: t => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
},
elastic: {
out: t => {
if (t === 0 || t === 1) return t
return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * (2 * Math.PI) / 3) + 1
}
},
bounce: {
out: t => {
const n1 = 7.5625
const d1 = 2.75
if (t < 1 / d1) return n1 * t * t
if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75
if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375
return n1 * (t -= 2.625 / d1) * t + 0.984375
}
},
get(name) {
const parts = name.split(/(?=[A-Z])/)
if (parts.length === 1) {
return this[parts[0].toLowerCase()]
}
const [type, direction] = parts
return this[type.toLowerCase()][direction.toLowerCase()]
}
}
const easing = EasingLibrary.get('easeOutBounce')
缓动动画的核心要点:
缓动动画是提升用户体验的重要技术,合理使用可以让界面更加生动自然。