学习Canvas弹性动画技术,掌握弹簧动画原理和弹性效果的实现方法。弹性动画模拟弹簧的物理特性,创造出有弹性的动画效果,常用于交互反馈和游戏UI。
弹性动画基于弹簧的物理模型,物体在目标位置附近振动,最终稳定在目标位置。
// 弹簧公式:F = -k * x
// F: 弹簧力
// k: 弹簧系数(劲度系数)
// x: 位移
class Spring {
constructor(options = {}) {
this.position = options.position || 0
this.target = options.target || 0
this.velocity = options.velocity || 0
this.stiffness = options.stiffness || 0.1 // 刚度
this.damping = options.damping || 0.8 // 阻尼
this.mass = options.mass || 1 // 质量
}
update() {
const force = (this.target - this.position) * this.stiffness
const acceleration = force / this.mass
this.velocity += acceleration
this.velocity *= this.damping
this.position += this.velocity
return this.position
}
}
刚度决定弹簧的"硬度",影响振动频率。
// 低刚度:缓慢、柔和
const softSpring = new Spring({ stiffness: 0.05 })
// 高刚度:快速、紧绷
const stiffSpring = new Spring({ stiffness: 0.3 })
阻尼决定能量消耗速度,影响振动持续时间。
// 低阻尼:振动时间长
const bouncySpring = new Spring({ damping: 0.9 })
// 高阻尼:快速停止
const quickSpring = new Spring({ damping: 0.5 })
质量影响惯性,较大的质量会延迟响应。
// 轻质量:快速响应
const lightSpring = new Spring({ mass: 0.5 })
// 重质量:缓慢响应
const heavySpring = new Spring({ mass: 2 })
class SpringAnimation {
constructor(options) {
this.from = options.from
this.to = options.to
this.stiffness = options.stiffness || 100
this.damping = options.damping || 10
this.mass = options.mass || 1
this.velocity = options.velocity || 0
this.onUpdate = options.onUpdate
this.onComplete = options.onComplete
this.current = this.from
this.running = false
this.settled = false
}
start() {
this.running = true
this.settled = false
this.tick()
return this
}
tick() {
if (!this.running || this.settled) return
const spring = this.stiffness
const damper = this.damping
const force = spring * (this.to - this.current)
const acceleration = (force - damper * this.velocity) / this.mass
this.velocity += acceleration * 0.016
this.current += this.velocity
if (this.onUpdate) {
this.onUpdate(this.current)
}
if (Math.abs(this.velocity) < 0.001 && Math.abs(this.to - this.current) < 0.001) {
this.current = this.to
this.settled = true
this.running = false
if (this.onComplete) {
this.onComplete()
}
return
}
requestAnimationFrame(() => this.tick())
}
stop() {
this.running = false
return this
}
reset() {
this.current = this.from
this.velocity = 0
this.settled = false
return this
}
}
const SpringPresets = {
gentle: {
stiffness: 120,
damping: 14
},
wobbly: {
stiffness: 180,
damping: 12
},
stiff: {
stiffness: 300,
damping: 20
},
slow: {
stiffness: 100,
damping: 22
},
bouncy: {
stiffness: 200,
damping: 10
}
}
function createSpring(preset, options = {}) {
const config = { ...SpringPresets[preset], ...options }
return new SpringAnimation(config)
}
const animation = createSpring('bouncy', {
from: 0,
to: 100,
onUpdate: (value) => console.log(value)
})
class SpringVector {
constructor(options) {
this.x = options.x || 0
this.y = options.y || 0
this.targetX = options.targetX || this.x
this.targetY = options.targetY || this.y
this.vx = 0
this.vy = 0
this.stiffness = options.stiffness || 0.1
this.damping = options.damping || 0.8
}
setTarget(x, y) {
this.targetX = x
this.targetY = y
}
update() {
const ax = (this.targetX - this.x) * this.stiffness
const ay = (this.targetY - this.y) * this.stiffness
this.vx = (this.vx + ax) * this.damping
this.vy = (this.vy + ay) * this.damping
this.x += this.vx
this.y += this.vy
}
isSettled() {
const dx = Math.abs(this.targetX - this.x)
const dy = Math.abs(this.targetY - this.y)
const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy)
return dx < 0.1 && dy < 0.1 && speed < 0.1
}
}
class ElasticFollow {
constructor(options) {
this.target = options.target
this.stiffness = options.stiffness || 0.1
this.damping = options.damping || 0.7
this.x = this.target.x
this.y = this.target.y
this.vx = 0
this.vy = 0
}
update() {
const ax = (this.target.x - this.x) * this.stiffness
const ay = (this.target.y - this.y) * this.stiffness
this.vx = (this.vx + ax) * this.damping
this.vy = (this.vy + ay) * this.damping
this.x += this.vx
this.y += this.vy
}
}
const mouse = { x: 200, y: 100 }
const follower = new ElasticFollow({
target: mouse,
stiffness: 0.08,
damping: 0.75
})
canvas.addEventListener('mousemove', (e) => {
mouse.x = e.offsetX
mouse.y = e.offsetY
})
class ElasticButton {
constructor(element, options = {}) {
this.element = element
this.scale = 1
this.targetScale = 1
this.velocity = 0
this.stiffness = options.stiffness || 0.3
this.damping = options.damping || 0.6
this.element.addEventListener('mousedown', () => this.press())
this.element.addEventListener('mouseup', () => this.release())
this.element.addEventListener('mouseleave', () => this.release())
this.animate()
}
press() {
this.targetScale = 0.9
}
release() {
this.targetScale = 1
}
animate() {
const force = (this.targetScale - this.scale) * this.stiffness
this.velocity = (this.velocity + force) * this.damping
this.scale += this.velocity
this.element.style.transform = `scale(${this.scale})`
requestAnimationFrame(() => this.animate())
}
}
class ElasticScroll {
constructor(container, options = {}) {
this.container = container
this.scrollTop = 0
this.targetScrollTop = 0
this.velocity = 0
this.stiffness = options.stiffness || 0.1
this.damping = options.damping || 0.7
container.addEventListener('scroll', () => {
this.targetScrollTop = container.scrollTop
})
this.animate()
}
animate() {
const force = (this.targetScrollTop - this.scrollTop) * this.stiffness
this.velocity = (this.velocity + force) * this.damping
this.scrollTop += this.velocity
// 使用 scrollTop 做其他事情
requestAnimationFrame(() => this.animate())
}
}
| 特性 | 弹性动画 | 缓动动画 |
|---|---|---|
| 物理模型 | 弹簧物理 | 数学曲线 |
| 超调 | 可以超过目标 | 不会超过目标 |
| 可中断 | 可以随时改变目标 | 需要重新开始 |
| 参数 | 刚度、阻尼、质量 | 缓动函数、时长 |
| 适用场景 | 交互反馈、拖拽 | UI过渡、动画 |
class OptimizedSpring {
constructor() {
this.position = 0
this.target = 0
this.velocity = 0
this.stiffness = 0.1
this.damping = 0.8
this.settled = true
}
update() {
if (this.settled) return this.position
const force = (this.target - this.position) * this.stiffness
this.velocity = (this.velocity + force) * this.damping
this.position += this.velocity
if (Math.abs(this.velocity) < 0.001 && Math.abs(this.target - this.position) < 0.001) {
this.position = this.target
this.settled = true
}
return this.position
}
setTarget(target) {
this.target = target
this.settled = false
}
}