Canvas 高级特性
本文档介绍 Canvas 的高级特性,包括合成操作、裁剪区域、动画系统等内容。
合成操作
globalCompositeOperation 属性决定新绘制的图形如何与已有内容混合。
属性值
javascript
ctx.globalCompositeOperation = 'source-over' // 默认值合成模式分类
基础模式
| 模式 | 说明 |
|---|---|
source-over | 新图形绘制在已有内容之上(默认) |
source-in | 只显示新图形与已有内容重叠的部分 |
source-out | 只显示新图形不与已有内容重叠的部分 |
source-atop | 新图形只在已有内容上显示 |
目标模式
| 模式 | 说明 |
|---|---|
destination-over | 新图形绘制在已有内容之下 |
destination-in | 只显示已有内容与新图形重叠的部分 |
destination-out | 只显示已有内容不与新图形重叠的部分 |
destination-atop | 已有内容只在新图形上显示 |
特殊模式
| 模式 | 说明 |
|---|---|
lighter | 颜色值相加 |
copy | 只显示新图形 |
xor | 异或操作 |
multiply | 正片叠底 |
screen | 滤色 |
overlay | 叠加 |
darken | 变暗 |
lighten | 变亮 |
color-dodge | 颜色减淡 |
color-burn | 颜色加深 |
hard-light | 强光 |
soft-light | 柔光 |
difference | 差值 |
exclusion | 排除 |
hue | 色相 |
saturation | 饱和度 |
color | 颜色 |
luminosity | 亮度 |
使用示例
基础示例
javascript
// 绘制基础图形
ctx.fillStyle = '#3498db'
ctx.fillRect(50, 50, 150, 150)
// 应用合成模式
ctx.globalCompositeOperation = 'multiply'
// 绘制叠加图形
ctx.fillStyle = '#e74c3c'
ctx.beginPath()
ctx.arc(175, 175, 75, 0, Math.PI * 2)
ctx.fill()
// 重置合成模式
ctx.globalCompositeOperation = 'source-over'橡皮擦效果
javascript
// 绘制背景
ctx.fillStyle = '#3498db'
ctx.fillRect(0, 0, 400, 300)
// 擦除部分内容
ctx.globalCompositeOperation = 'destination-out'
ctx.beginPath()
ctx.arc(200, 150, 80, 0, Math.PI * 2)
ctx.fill()
// 重置
ctx.globalCompositeOperation = 'source-over'光晕效果
javascript
ctx.globalCompositeOperation = 'lighter'
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'
ctx.beginPath()
ctx.arc(150, 150, 80, 0, Math.PI * 2)
ctx.fill()
ctx.fillStyle = 'rgba(0, 255, 0, 0.5)'
ctx.beginPath()
ctx.arc(200, 150, 80, 0, Math.PI * 2)
ctx.fill()
ctx.fillStyle = 'rgba(0, 0, 255, 0.5)'
ctx.beginPath()
ctx.arc(175, 200, 80, 0, Math.PI * 2)
ctx.fill()
ctx.globalCompositeOperation = 'source-over'裁剪区域
clip() 方法将当前路径设置为裁剪区域,之后绘制的图形只在该区域内可见。
基本用法
javascript
// 创建裁剪路径
ctx.beginPath()
ctx.arc(200, 150, 100, 0, Math.PI * 2)
ctx.clip()
// 绘制的图形只显示在圆形区域内
ctx.fillStyle = '#3498db'
ctx.fillRect(0, 0, 400, 300)裁剪矩形
javascript
// 裁剪为矩形区域
ctx.beginPath()
ctx.rect(50, 50, 200, 150)
ctx.clip()
// 绘制渐变背景
const gradient = ctx.createLinearGradient(0, 0, 400, 300)
gradient.addColorStop(0, '#3498db')
gradient.addColorStop(1, '#2ecc71')
ctx.fillStyle = gradient
ctx.fillRect(0, 0, 400, 300)复杂裁剪路径
javascript
// 创建星形裁剪路径
function createStarPath(cx, cy, spikes, outerRadius, innerRadius) {
ctx.beginPath()
for (let i = 0; i < spikes * 2; i++) {
const radius = i % 2 === 0 ? outerRadius : innerRadius
const angle = (i * Math.PI) / spikes - Math.PI / 2
const x = cx + radius * Math.cos(angle)
const y = cy + radius * Math.sin(angle)
if (i === 0) {
ctx.moveTo(x, y)
} else {
ctx.lineTo(x, y)
}
}
ctx.closePath()
}
createStarPath(200, 150, 5, 100, 50)
ctx.clip()
// 绘制内容
const gradient = ctx.createLinearGradient(0, 0, 400, 300)
gradient.addColorStop(0, '#f39c12')
gradient.addColorStop(1, '#e74c3c')
ctx.fillStyle = gradient
ctx.fillRect(0, 0, 400, 300)取消裁剪
javascript
ctx.save()
// 创建裁剪区域
ctx.beginPath()
ctx.arc(200, 150, 100, 0, Math.PI * 2)
ctx.clip()
// 绘制裁剪内容
ctx.fillRect(0, 0, 400, 300)
// 取消裁剪
ctx.restore()
// 现在可以正常绘制
ctx.fillStyle = 'red'
ctx.fillRect(300, 200, 50, 50)动画系统
requestAnimationFrame
requestAnimationFrame 是浏览器提供的动画 API,会在下次重绘之前调用指定函数。
javascript
function animate() {
// 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 更新状态
update()
// 绘制
draw()
// 继续动画循环
requestAnimationFrame(animate)
}
animate()基础动画示例
javascript
let x = 0
const speed = 2
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 更新位置
x += speed
if (x > canvas.width) {
x = -50
}
// 绘制
ctx.fillStyle = '#3498db'
ctx.fillRect(x, 100, 50, 50)
requestAnimationFrame(animate)
}
animate()旋转动画
javascript
let angle = 0
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.save()
ctx.translate(200, 150)
ctx.rotate(angle)
ctx.fillStyle = '#e74c3c'
ctx.fillRect(-40, -40, 80, 80)
ctx.restore()
angle += 0.02
requestAnimationFrame(animate)
}
animate()弹跳动画
javascript
let y = 0
let velocity = 0
const gravity = 0.5
const bounce = -0.8
const ground = 250
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 应用重力
velocity += gravity
y += velocity
// 碰撞检测
if (y > ground) {
y = ground
velocity *= bounce
}
// 绘制球
ctx.fillStyle = '#3498db'
ctx.beginPath()
ctx.arc(200, y, 20, 0, Math.PI * 2)
ctx.fill()
requestAnimationFrame(animate)
}
animate()粒子系统
javascript
class Particle {
constructor(x, y) {
this.x = x
this.y = y
this.vx = (Math.random() - 0.5) * 4
this.vy = (Math.random() - 0.5) * 4
this.life = 1
this.decay = Math.random() * 0.02 + 0.01
this.color = `hsl(${Math.random() * 360}, 70%, 60%)`
}
update() {
this.x += this.vx
this.y += this.vy
this.life -= this.decay
}
draw(ctx) {
ctx.globalAlpha = this.life
ctx.fillStyle = this.color
ctx.beginPath()
ctx.arc(this.x, this.y, 3, 0, Math.PI * 2)
ctx.fill()
ctx.globalAlpha = 1
}
}
const particles = []
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 添加新粒子
if (Math.random() > 0.5) {
particles.push(new Particle(200, 150))
}
// 更新和绘制粒子
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update()
particles[i].draw(ctx)
// 移除死亡粒子
if (particles[i].life <= 0) {
particles.splice(i, 1)
}
}
requestAnimationFrame(animate)
}
animate()缓动函数
缓动函数可以让动画更自然流畅。
常用缓动函数
javascript
const easing = {
// 线性
linear: t => t,
// 缓入(加速)
easeInQuad: t => t * t,
easeInCubic: t => t * t * t,
easeInQuart: t => t * t * t * t,
easeInExpo: t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1)),
easeInCirc: t => 1 - Math.sqrt(1 - t * t),
easeInBack: t => {
const c = 1.70158
return t * t * ((c + 1) * t - c)
},
// 缓出(减速)
easeOutQuad: t => t * (2 - t),
easeOutCubic: t => (--t) * t * t + 1,
easeOutQuart: t => 1 - (--t) * t * t * t,
easeOutExpo: t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t),
easeOutCirc: t => Math.sqrt(1 - (--t) * t),
easeOutBack: t => {
const c = 1.70158
return (--t) * t * ((c + 1) * t + c) + 1
},
// 缓入缓出
easeInOutQuad: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
easeInOutCubic: t => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
easeInOutQuart: t => t < 0.5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t,
easeInOutExpo: t => {
if (t === 0 || t === 1) return t
return t < 0.5
? Math.pow(2, 20 * t - 10) / 2
: (2 - Math.pow(2, -20 * t + 10)) / 2
},
easeInOutCirc: t => t < 0.5
? (1 - Math.sqrt(1 - 4 * t * t)) / 2
: (Math.sqrt(1 - Math.pow(-2 * t + 2, 2)) + 1) / 2,
// 弹性
easeOutElastic: 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
},
// 弹跳
easeOutBounce: 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
}
}
}缓动函数可视化
code
线性 (linear)
│╱
│ ╱
│ ╱
│ ╱
└────────
缓入 (easeIn)
│ ╱
│ ╱
│ ╱
│ ╱
│ ╱
│╱
└────────
缓出 (easeOut)
│╲
│ ╲
│ ╲
│ ╲
│ ╲
│ ╲
└────────
缓入缓出 (easeInOut)
│ ╱
│ ╱
│ ╱
│╱
│╲
│ ╲
└────────使用缓动动画
javascript
function animateWithEasing(start, end, duration, easingFn, callback) {
const startTime = performance.now()
function update(currentTime) {
const elapsed = currentTime - startTime
const progress = Math.min(elapsed / duration, 1)
const easedProgress = easingFn(progress)
const currentValue = start + (end - start) * easedProgress
callback(currentValue, progress)
if (progress < 1) {
requestAnimationFrame(update)
}
}
requestAnimationFrame(update)
}
// 使用示例
animateWithEasing(0, 300, 2000, easing.easeOutElastic, (value, progress) => {
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = '#3498db'
ctx.fillRect(value, 100, 50, 50)
})动画队列
javascript
class AnimationQueue {
constructor() {
this.queue = []
this.running = false
}
add(animation) {
this.queue.push(animation)
if (!this.running) {
this.runNext()
}
}
runNext() {
if (this.queue.length === 0) {
this.running = false
return
}
this.running = true
const animation = this.queue.shift()
animation(() => this.runNext())
}
}
// 使用示例
const queue = new AnimationQueue()
queue.add(next => {
animateWithEasing(0, 100, 1000, easing.easeOutQuad, (value, progress) => {
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.fillRect(value, 100, 50, 50)
if (progress >= 1) next()
})
})
queue.add(next => {
animateWithEasing(100, 200, 1000, easing.easeInQuad, (value, progress) => {
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.fillRect(value, 100, 50, 50)
if (progress >= 1) next()
})
})综合示例
游戏循环
一个完整的游戏循环需要处理时间、更新状态和渲染:
javascript
class GameLoop {
constructor(update, render) {
this.update = update // 更新函数
this.render = render // 渲染函数
this.running = false
this.lastTime = 0
this.accumulator = 0
this.fixedDeltaTime = 1 / 60 // 固定时间步长
}
start() {
if (this.running) return
this.running = true
this.lastTime = performance.now()
this.loop()
}
stop() {
this.running = false
}
loop() {
if (!this.running) return
const currentTime = performance.now()
const deltaTime = (currentTime - this.lastTime) / 1000
this.lastTime = currentTime
// 累积时间
this.accumulator += deltaTime
// 固定时间步长更新(保证物理模拟稳定)
while (this.accumulator >= this.fixedDeltaTime) {
this.update(this.fixedDeltaTime)
this.accumulator -= this.fixedDeltaTime
}
// 渲染(可传入插值因子实现平滑渲染)
const alpha = this.accumulator / this.fixedDeltaTime
this.render(alpha)
requestAnimationFrame(() => this.loop())
}
}
// 使用示例
const gameLoop = new GameLoop(
// 更新函数
(dt) => {
// 更新游戏对象
player.update(dt)
enemies.forEach(enemy => enemy.update(dt))
},
// 渲染函数
(alpha) => {
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 使用插值进行平滑渲染
const interpolatedX = player.previousX + (player.x - player.previousX) * alpha
const interpolatedY = player.previousY + (player.y - player.previousY) * alpha
ctx.fillStyle = '#3498db'
ctx.fillRect(interpolatedX, interpolatedY, 50, 50)
}
)
gameLoop.start()时间管理器
javascript
class TimeManager {
constructor() {
this.timeScale = 1 // 时间缩放(慢动作/快进)
this.paused = false
this.lastTime = 0
this.deltaTime = 0
this.elapsedTime = 0
this.frameCount = 0
this.fps = 0
this.fpsUpdateInterval = 1000
this.lastFpsUpdate = 0
this.framesSinceLastUpdate = 0
}
update(currentTime) {
if (this.paused) {
this.deltaTime = 0
return
}
// 计算增量时间
this.deltaTime = ((currentTime - this.lastTime) / 1000) * this.timeScale
this.lastTime = currentTime
// 累计时间
this.elapsedTime += this.deltaTime
this.frameCount++
// 计算 FPS
this.framesSinceLastUpdate++
if (currentTime - this.lastFpsUpdate >= this.fpsUpdateInterval) {
this.fps = this.framesSinceLastUpdate
this.framesSinceLastUpdate = 0
this.lastFpsUpdate = currentTime
}
}
pause() {
this.paused = true
}
resume() {
this.paused = false
this.lastTime = performance.now()
}
setSlowMotion(factor) {
this.timeScale = factor // 0.5 = 半速,2 = 双倍速
}
resetTimeScale() {
this.timeScale = 1
}
}
// 使用示例
const time = new TimeManager()
function gameLoop() {
const currentTime = performance.now()
time.update(currentTime)
// 使用 deltaTime 更新
updateGame(time.deltaTime)
// 显示 FPS
ctx.fillStyle = '#000'
ctx.font = '14px Arial'
ctx.fillText(`FPS: ${time.fps}`, 10, 20)
ctx.fillText(`Time: ${time.elapsedTime.toFixed(1)}s`, 10, 40)
requestAnimationFrame(gameLoop)
}
// 慢动作效果
function triggerSlowMotion() {
time.setSlowMotion(0.2)
setTimeout(() => time.resetTimeScale(), 1000)
}定时器管理
javascript
class TimerManager {
constructor() {
this.timers = []
}
// 延迟执行
setTimeout(callback, delay) {
const timer = {
callback,
delay,
elapsed: 0,
repeat: false,
active: true
}
this.timers.push(timer)
return timer
}
// 重复执行
setInterval(callback, interval) {
const timer = {
callback,
delay: interval,
elapsed: 0,
repeat: true,
active: true
}
this.timers.push(timer)
return timer
}
// 更新所有定时器
update(deltaTime) {
for (let i = this.timers.length - 1; i >= 0; i--) {
const timer = this.timers[i]
if (!timer.active) {
this.timers.splice(i, 1)
continue
}
timer.elapsed += deltaTime * 1000
if (timer.elapsed >= timer.delay) {
timer.callback()
if (timer.repeat) {
timer.elapsed -= timer.delay
} else {
this.timers.splice(i, 1)
}
}
}
}
// 清除定时器
clear(timer) {
timer.active = false
}
// 清除所有定时器
clearAll() {
this.timers = []
}
}
// 使用示例
const timers = new TimerManager()
// 延迟执行
timers.setTimeout(() => {
console.log('2秒后执行')
}, 2000)
// 重复执行
const spawnTimer = timers.setInterval(() => {
spawnEnemy()
}, 1000)
// 停止重复执行
timers.clear(spawnTimer)
// 在游戏循环中更新
function gameLoop() {
timers.update(time.deltaTime)
// ...
}太阳系动画
javascript
const sun = { x: 300, y: 250, radius: 30, color: '#f39c12' }
const planets = [
{ distance: 60, radius: 8, speed: 0.02, color: '#95a5a6', angle: 0 },
{ distance: 100, radius: 12, speed: 0.015, color: '#3498db', angle: Math.PI },
{ distance: 150, radius: 10, speed: 0.01, color: '#e74c3c', angle: Math.PI / 2 },
{ distance: 200, radius: 18, speed: 0.008, color: '#f39c12', angle: Math.PI * 1.5 }
]
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 绘制太阳
ctx.fillStyle = sun.color
ctx.beginPath()
ctx.arc(sun.x, sun.y, sun.radius, 0, Math.PI * 2)
ctx.fill()
// 绘制行星
planets.forEach(planet => {
planet.angle += planet.speed
const x = sun.x + Math.cos(planet.angle) * planet.distance
const y = sun.y + Math.sin(planet.angle) * planet.distance
// 轨道
ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)'
ctx.beginPath()
ctx.arc(sun.x, sun.y, planet.distance, 0, Math.PI * 2)
ctx.stroke()
// 行星
ctx.fillStyle = planet.color
ctx.beginPath()
ctx.arc(x, y, planet.radius, 0, Math.PI * 2)
ctx.fill()
})
requestAnimationFrame(animate)
}
animate()常见问题
1. 动画卡顿
问题: 动画不够流畅。
解决方案:
- 使用
requestAnimationFrame而非setInterval - 减少
clearRect的范围 - 使用离屏 Canvas 缓存静态内容
2. 裁剪性能问题
问题: 复杂裁剪路径影响性能。
解决方案: 使用简单的裁剪形状或缓存裁剪结果。
3. 合成模式不生效
问题: globalCompositeOperation 设置无效。
解决方案: 确保已有内容存在,且在绘制新内容前设置。
下一步学习
返回:Canvas 教程目录 | 上一篇:变换操作