深入学习Canvas分层渲染技术,通过多Canvas分层减少重绘区域提升性能 分层渲染是一种重要的Canvas性能优化技术,通过将不同类型的内容绘制在不同的Canvas层上,减少不必要的重绘操作。
在单Canvas架构中,任何内容的变化都需要重绘整个Canvas。分层渲染通过将内容分配到不同的层,只重绘发生变化的层,从而提升性能。
class LayeredCanvas {
constructor(container, options = {}) {
this.container = typeof container === 'string'
? document.querySelector(container)
: container
this.options = {
width: 800,
height: 600,
layers: ['background', 'main', 'ui'],
...options
}
this.layers = new Map()
this.layerOrder = []
this.init()
}
init() {
this.options.layers.forEach((name, index) => {
this.createLayer(name, index)
})
}
createLayer(name, zIndex) {
const canvas = document.createElement('canvas')
canvas.width = this.options.width
canvas.height = this.options.height
canvas.style.cssText = `
position: absolute;
top: 0;
left: 0;
z-index: ${zIndex};
pointer-events: ${name === 'ui' ? 'auto' : 'none'};
`
this.container.style.position = 'relative'
this.container.appendChild(canvas)
this.layers.set(name, {
canvas,
ctx: canvas.getContext('2d'),
zIndex,
visible: true,
dirty: true
})
this.layerOrder.push(name)
}
getLayer(name) {
return this.layers.get(name)
}
getContext(name) {
const layer = this.layers.get(name)
return layer ? layer.ctx : null
}
markDirty(name) {
const layer = this.layers.get(name)
if (layer) {
layer.dirty = true
}
}
clearLayer(name) {
const layer = this.layers.get(name)
if (layer) {
layer.ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height)
layer.dirty = true
}
}
setLayerVisibility(name, visible) {
const layer = this.layers.get(name)
if (layer) {
layer.visible = visible
layer.canvas.style.display = visible ? 'block' : 'none'
}
}
resize(width, height) {
this.options.width = width
this.options.height = height
this.layers.forEach(layer => {
layer.canvas.width = width
layer.canvas.height = height
layer.dirty = true
})
}
destroy() {
this.layers.forEach(layer => {
layer.canvas.remove()
})
this.layers.clear()
this.layerOrder = []
}
}
class FrequencyBasedLayering {
constructor(container) {
this.layeredCanvas = new LayeredCanvas(container, {
layers: ['static', 'dynamic', 'overlay']
})
this.staticLayer = this.layeredCanvas.getLayer('static')
this.dynamicLayer = this.layeredCanvas.getLayer('dynamic')
this.overlayLayer = this.layeredCanvas.getLayer('overlay')
this.staticContent = []
this.dynamicContent = []
this.overlayContent = []
this.staticRendered = false
}
addStaticContent(renderFn) {
this.staticContent.push(renderFn)
this.staticRendered = false
}
addDynamicContent(renderFn) {
this.dynamicContent.push(renderFn)
}
addOverlayContent(renderFn) {
this.overlayContent.push(renderFn)
}
render() {
if (!this.staticRendered) {
this.renderStatic()
this.staticRendered = true
}
this.renderDynamic()
this.renderOverlay()
}
renderStatic() {
const ctx = this.staticLayer.ctx
ctx.clearRect(0, 0, this.staticLayer.canvas.width, this.staticLayer.canvas.height)
this.staticContent.forEach(renderFn => renderFn(ctx))
}
renderDynamic() {
const ctx = this.dynamicLayer.ctx
ctx.clearRect(0, 0, this.dynamicLayer.canvas.width, this.dynamicLayer.canvas.height)
this.dynamicContent.forEach(renderFn => renderFn(ctx))
}
renderOverlay() {
const ctx = this.overlayLayer.ctx
ctx.clearRect(0, 0, this.overlayLayer.canvas.width, this.overlayLayer.canvas.height)
this.overlayContent.forEach(renderFn => renderFn(ctx))
}
invalidateStatic() {
this.staticRendered = false
}
}
class GameLayerManager {
constructor(container) {
this.layeredCanvas = new LayeredCanvas(container, {
layers: ['background', 'terrain', 'entities', 'effects', 'ui']
})
this.initLayers()
}
initLayers() {
this.layers = {
background: this.layeredCanvas.getContext('background'),
terrain: this.layeredCanvas.getContext('terrain'),
entities: this.layeredCanvas.getContext('entities'),
effects: this.layeredCanvas.getContext('effects'),
ui: this.layeredCanvas.getContext('ui')
}
}
renderBackground(drawFn) {
const ctx = this.layers.background
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
drawFn(ctx)
}
renderTerrain(drawFn) {
const ctx = this.layers.terrain
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
drawFn(ctx)
}
renderEntities(entities) {
const ctx = this.layers.entities
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
entities.forEach(entity => {
entity.render(ctx)
})
}
renderEffects(effects) {
const ctx = this.layers.effects
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
effects.forEach(effect => {
effect.render(ctx)
})
}
renderUI(ui) {
const ctx = this.layers.ui
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
ui.forEach(element => {
element.render(ctx)
})
}
}
class ParallaxLayer {
constructor(canvas, options = {}) {
this.canvas = canvas
this.ctx = canvas.getContext('2d')
this.options = {
speedFactor: 1,
direction: 1,
repeat: true,
...options
}
this.scrollX = 0
this.scrollY = 0
this.content = null
this.contentWidth = 0
this.contentHeight = 0
}
setContent(drawable, width, height) {
this.content = drawable
this.contentWidth = width
this.contentHeight = height
}
update(deltaX, deltaY) {
this.scrollX += deltaX * this.options.speedFactor * this.options.direction
this.scrollY += deltaY * this.options.speedFactor * this.options.direction
}
render() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
if (!this.content) return
if (this.options.repeat) {
this.renderRepeating()
} else {
this.renderSingle()
}
}
renderRepeating() {
const startX = this.scrollX % this.contentWidth - this.contentWidth
const startY = this.scrollY % this.contentHeight - this.contentHeight
for (let x = startX; x < this.canvas.width + this.contentWidth; x += this.contentWidth) {
for (let y = startY; y < this.canvas.height + this.contentHeight; y += this.contentHeight) {
this.ctx.drawImage(this.content, x, y)
}
}
}
renderSingle() {
this.ctx.drawImage(this.content, -this.scrollX, -this.scrollY)
}
}
class ParallaxScene {
constructor(container) {
this.container = container
this.layers = []
this.canvasWidth = 0
this.canvasHeight = 0
this.init()
}
init() {
const rect = this.container.getBoundingClientRect()
this.canvasWidth = rect.width
this.canvasHeight = rect.height
}
addLayer(options = {}) {
const canvas = document.createElement('canvas')
canvas.width = this.canvasWidth
canvas.height = this.canvasHeight
canvas.style.cssText = 'position: absolute; top: 0; left: 0;'
this.container.style.position = 'relative'
this.container.appendChild(canvas)
const layer = new ParallaxLayer(canvas, options)
this.layers.push(layer)
this.updateLayerOrder()
return layer
}
updateLayerOrder() {
this.layers.forEach((layer, index) => {
layer.canvas.style.zIndex = index
})
}
update(deltaX, deltaY) {
this.layers.forEach(layer => {
layer.update(deltaX, deltaY)
layer.render()
})
}
resize(width, height) {
this.canvasWidth = width
this.canvasHeight = height
this.layers.forEach(layer => {
layer.canvas.width = width
layer.canvas.height = height
layer.render()
})
}
}
class InteractiveLayerManager {
constructor(container) {
this.layeredCanvas = new LayeredCanvas(container, {
layers: ['background', 'game', 'ui']
})
this.eventHandlers = new Map()
this.interactiveObjects = new Map()
this.setupEvents()
}
setupEvents() {
const container = this.layeredCanvas.container
container.addEventListener('click', (e) => this.handleClick(e))
container.addEventListener('mousemove', (e) => this.handleMouseMove(e))
container.addEventListener('mousedown', (e) => this.handleMouseDown(e))
container.addEventListener('mouseup', (e) => this.handleMouseUp(e))
}
getCanvasCoordinates(e, canvas) {
const rect = canvas.getBoundingClientRect()
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top
}
}
handleClick(e) {
const uiLayer = this.layeredCanvas.getLayer('ui')
const coords = this.getCanvasCoordinates(e, uiLayer.canvas)
if (this.checkUIInteraction(coords)) {
return
}
const gameLayer = this.layeredCanvas.getLayer('game')
const gameCoords = this.getCanvasCoordinates(e, gameLayer.canvas)
this.checkGameInteraction(gameCoords)
}
handleMouseMove(e) {
const gameLayer = this.layeredCanvas.getLayer('game')
const coords = this.getCanvasCoordinates(e, gameLayer.canvas)
this.interactiveObjects.forEach((obj, key) => {
if (obj.layer === 'game' && obj.containsPoint(coords.x, coords.y)) {
obj.onHover?.(coords)
}
})
}
handleMouseDown(e) {
const gameLayer = this.layeredCanvas.getLayer('game')
const coords = this.getCanvasCoordinates(e, gameLayer.canvas)
this.interactiveObjects.forEach((obj, key) => {
if (obj.layer === 'game' && obj.containsPoint(coords.x, coords.y)) {
obj.onPress?.(coords)
}
})
}
handleMouseUp(e) {
const gameLayer = this.layeredCanvas.getLayer('game')
const coords = this.getCanvasCoordinates(e, gameLayer.canvas)
this.interactiveObjects.forEach((obj, key) => {
if (obj.layer === 'game') && obj.containsPoint(coords.x, coords.y)) {
obj.onRelease?.(coords)
}
})
}
checkUIInteraction(coords) {
let handled = false
this.interactiveObjects.forEach((obj, key) => {
if (obj.layer === 'ui' && obj.containsPoint(coords.x, coords.y)) {
obj.onClick?.(coords)
handled = true
}
})
return handled
}
checkGameInteraction(coords) {
this.interactiveObjects.forEach((obj, key) => {
if (obj.layer === 'game' && obj.containsPoint(coords.x, coords.y)) {
obj.onClick?.(coords)
}
})
}
registerInteractiveObject(key, obj) {
this.interactiveObjects.set(key, obj)
}
unregisterInteractiveObject(key) {
this.interactiveObjects.delete(key)
}
}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas 分层渲染 - 游戏场景</title>
<style>
body {
margin: 0;
padding: 20px;
background: #1a1a2e;
font-family: system-ui, sans-serif;
}
.container {
max-width: 900px;
margin: 0 auto;
}
h1 {
color: #eee;
text-align: center;
}
.game-container {
position: relative;
width: 800px;
height: 500px;
margin: 0 auto;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
}
.game-container canvas {
position: absolute;
top: 0;
left: 0;
}
.controls {
display: flex;
gap: 10px;
justify-content: center;
margin-top: 20px;
}
button {
padding: 10px 20px;
background: #e94560;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: #ff6b6b;
}
.info {
color: #aaa;
text-align: center;
margin-top: 10px;
font-size: 14px;
}
</style>
</head>
<body>
<div class="container">
<h1>游戏场景分层渲染</h1>
<div class="game-container" id="gameContainer"></div>
<div class="controls">
<button onclick="toggleAnimation()">暂停/继续</button>
<button onclick="addEffect()">添加特效</button>
<button onclick="toggleUI()">显示/隐藏UI</button>
</div>
<div class="info">使用方向键移动角色</div>
</div>
<script>
class GameScene {
constructor(container) {
this.container = container
this.width = 800
this.height = 500
this.layers = {}
this.createLayers()
this.player = {
x: this.width / 2,
y: this.height / 2,
width: 40,
height: 40,
speed: 5,
color: '#e94560'
}
this.enemies = []
this.effects = []
this.stars = []
this.keys = {}
this.isRunning = true
this.uiVisible = true
this.score = 0
this.time = 0
this.init()
}
createLayers() {
const layerNames = ['background', 'game', 'effects', 'ui']
layerNames.forEach((name, index) => {
const canvas = document.createElement('canvas')
canvas.width = this.width
canvas.height = this.height
canvas.style.zIndex = index
canvas.style.pointerEvents = name === 'ui' ? 'auto' : 'none'
this.container.appendChild(canvas)
this.layers[name] = {
canvas,
ctx: canvas.getContext('2d'),
dirty: true
}
})
}
init() {
this.createStars()
this.createEnemies()
this.setupInput()
this.renderBackground()
}
createStars() {
for (let i = 0; i < 100; i++) {
this.stars.push({
x: Math.random() * this.width,
y: Math.random() * this.height,
size: Math.random() * 2 + 1,
speed: Math.random() * 0.5 + 0.1,
brightness: Math.random()
})
}
}
createEnemies() {
for (let i = 0; i < 5; i++) {
this.enemies.push({
x: Math.random() * (this.width - 30),
y: Math.random() * (this.height - 30),
width: 30,
height: 30,
vx: (Math.random() - 0.5) * 3,
vy: (Math.random() - 0.5) * 3,
color: '#4ecdc4'
})
}
}
setupInput() {
document.addEventListener('keydown', (e) => {
this.keys[e.key] = true
})
document.addEventListener('keyup', (e) => {
this.keys[e.key] = false
})
}
update() {
this.time += 1/60
if (this.keys['ArrowLeft']) this.player.x -= this.player.speed
if (this.keys['ArrowRight']) this.player.x += this.player.speed
if (this.keys['ArrowUp']) this.player.y -= this.player.speed
if (this.keys['ArrowDown']) this.player.y += this.player.speed
this.player.x = Math.max(0, Math.min(this.width - this.player.width, this.player.x))
this.player.y = Math.max(0, Math.min(this.height - this.player.height, this.player.y))
this.enemies.forEach(enemy => {
enemy.x += enemy.vx
enemy.y += enemy.vy
if (enemy.x <= 0 || enemy.x >= this.width - enemy.width) enemy.vx *= -1
if (enemy.y <= 0 || enemy.y >= this.height - enemy.height) enemy.vy *= -1
})
this.stars.forEach(star => {
star.y += star.speed
if (star.y > this.height) {
star.y = 0
star.x = Math.random() * this.width
}
star.brightness = 0.5 + Math.sin(this.time * 3 + star.x) * 0.5
})
this.effects = this.effects.filter(effect => {
effect.life -= 1/60
effect.particles.forEach(p => {
p.x += p.vx
p.y += p.vy
p.alpha -= 0.02
})
return effect.life > 0
})
}
renderBackground() {
const ctx = this.layers.background.ctx
const gradient = ctx.createLinearGradient(0, 0, 0, this.height)
gradient.addColorStop(0, '#0f0f23')
gradient.addColorStop(1, '#1a1a2e')
ctx.fillStyle = gradient
ctx.fillRect(0, 0, this.width, this.height)
}
renderGame() {
const ctx = this.layers.game.ctx
ctx.clearRect(0, 0, this.width, this.height)
this.stars.forEach(star => {
ctx.fillStyle = `rgba(255, 255, 255, ${star.brightness})`
ctx.beginPath()
ctx.arc(star.x, star.y, star.size, 0, Math.PI * 2)
ctx.fill()
})
this.enemies.forEach(enemy => {
ctx.fillStyle = enemy.color
ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height)
ctx.fillStyle = '#fff'
ctx.fillRect(enemy.x + 5, enemy.y + 8, 6, 6)
ctx.fillRect(enemy.x + 19, enemy.y + 8, 6, 6)
})
ctx.fillStyle = this.player.color
ctx.fillRect(this.player.x, this.player.y, this.player.width, this.player.height)
ctx.fillStyle = '#fff'
ctx.beginPath()
ctx.arc(this.player.x + 12, this.player.y + 15, 5, 0, Math.PI * 2)
ctx.arc(this.player.x + 28, this.player.y + 15, 5, 0, Math.PI * 2)
ctx.fill()
ctx.fillStyle = '#333'
ctx.beginPath()
ctx.arc(this.player.x + 12, this.player.y + 15, 2, 0, Math.PI * 2)
ctx.arc(this.player.x + 28, this.player.y + 15, 2, 0, Math.PI * 2)
ctx.fill()
}
renderEffects() {
const ctx = this.layers.effects.ctx
ctx.clearRect(0, 0, this.width, this.height)
this.effects.forEach(effect => {
effect.particles.forEach(p => {
ctx.fillStyle = `rgba(${p.color}, ${p.alpha})`
ctx.beginPath()
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2)
ctx.fill()
})
})
}
renderUI() {
const ctx = this.layers.ui.ctx
ctx.clearRect(0, 0, this.width, this.height)
if (!this.uiVisible) return
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'
ctx.fillRect(10, 10, 150, 70)
ctx.fillStyle = '#fff'
ctx.font = '16px Arial'
ctx.fillText(`分数: ${this.score}`, 20, 35)
ctx.fillText(`时间: ${this.time.toFixed(1)}s`, 20, 60)
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'
ctx.fillRect(this.width - 110, 10, 100, 40)
ctx.fillStyle = '#4ecdc4'
ctx.font = '14px Arial'
ctx.fillText('按空格暂停', this.width - 100, 35)
}
render() {
this.renderGame()
this.renderEffects()
this.renderUI()
}
addEffect() {
const effect = {
life: 1,
particles: []
}
for (let i = 0; i < 20; i++) {
effect.particles.push({
x: this.player.x + this.player.width / 2,
y: this.player.y + this.player.height / 2,
vx: (Math.random() - 0.5) * 8,
vy: (Math.random() - 0.5) * 8,
size: Math.random() * 4 + 2,
color: Math.random() > 0.5 ? '233, 69, 96' : '78, 205, 196',
alpha: 1
})
}
this.effects.push(effect)
this.score += 10
}
toggleUI() {
this.uiVisible = !this.uiVisible
}
toggle() {
this.isRunning = !this.isRunning
}
loop() {
if (this.isRunning) {
this.update()
this.render()
}
requestAnimationFrame(() => this.loop())
}
start() {
this.loop()
}
}
const game = new GameScene(document.getElementById('gameContainer'))
game.start()
function toggleAnimation() {
game.toggle()
}
function addEffect() {
game.addEffect()
}
function toggleUI() {
game.toggleUI()
}
</script>
</body>
</html>
const layeringPrinciples = {
byUpdateFrequency: {
description: '按更新频率分层',
layers: [
{ name: 'static', update: 'never', example: '背景、地图' },
{ name: 'rare', update: '偶尔', example: '地形变化' },
{ name: 'frequent', update: '每帧', example: '角色、粒子' },
{ name: 'overlay', update: '按需', example: 'UI、提示' }
]
},
byFunction: {
description: '按功能分层',
layers: [
{ name: 'background', purpose: '静态背景' },
{ name: 'terrain', purpose: '地形/地图' },
{ name: 'entities', purpose: '游戏对象' },
{ name: 'effects', purpose: '特效' },
{ name: 'ui', purpose: '用户界面' }
]
},
byInteraction: {
description: '按交互性分层',
layers: [
{ name: 'non-interactive', pointerEvents: 'none' },
{ name: 'interactive', pointerEvents: 'auto' }
]
}
}
class LayerPerformanceMonitor {
constructor(layeredCanvas) {
this.layeredCanvas = layeredCanvas
this.metrics = new Map()
}
startMeasure(layerName) {
if (!this.metrics.has(layerName)) {
this.metrics.set(layerName, {
renderTime: 0,
renderCount: 0,
avgTime: 0
})
}
this.metrics.get(layerName).startTime = performance.now()
}
endMeasure(layerName) {
const metric = this.metrics.get(layerName)
if (metric && metric.startTime) {
const elapsed = performance.now() - metric.startTime
metric.renderTime += elapsed
metric.renderCount++
metric.avgTime = metric.renderTime / metric.renderCount
}
}
getReport() {
const report = []
this.metrics.forEach((metric, name) => {
report.push({
layer: name,
avgRenderTime: `${metric.avgTime.toFixed(2)}ms`,
totalRenders: metric.renderCount
})
})
return report
}
}