{T}

Canvas 常见问题

本文档收集 Canvas 开发中的常见问题及其解决方案。

显示问题

Canvas 显示模糊

问题描述: 在高 DPI 屏幕上,Canvas 内容显示模糊。

原因: Canvas 的实际分辨率与 CSS 显示尺寸不匹配。

解决方案:

javascript
function setupHiDPICanvas(canvas) {
  const dpr = window.devicePixelRatio || 1
  const rect = canvas.getBoundingClientRect()
  
  // 设置实际分辨率
  canvas.width = rect.width * dpr
  canvas.height = rect.height * dpr
  
  // 设置显示尺寸
  canvas.style.width = rect.width + 'px'
  canvas.style.height = rect.height + 'px'
  
  // 缩放上下文
  const ctx = canvas.getContext('2d')
  ctx.scale(dpr, dpr)
  
  return ctx
}

图形边缘锯齿

问题描述: 线条或图形边缘有锯齿。

解决方案:

javascript
// 方法1:偏移 0.5 像素
ctx.moveTo(50.5, 10.5)
ctx.lineTo(200.5, 10.5)

// 方法2:使用更粗的线条
ctx.lineWidth = 2

// 方法3:启用抗锯齿(默认启用)
// Canvas 默认启用抗锯齿

线条宽度不一致

问题描述: 1px 线条看起来是 2px。

原因: 线条以中心点绘制,半像素导致模糊。

解决方案:

javascript
// 奇数线宽使用半像素偏移
if (lineWidth % 2 === 1) {
  ctx.translate(0.5, 0.5)
}

图形不显示

问题描述: 绘制了图形但看不到。

排查步骤:

javascript
// 1. 检查坐标是否在画布范围内
console.log('Canvas size:', canvas.width, canvas.height)
console.log('Draw position:', x, y)

// 2. 检查样式是否设置
console.log('Fill style:', ctx.fillStyle)
console.log('Stroke style:', ctx.strokeStyle)

// 3. 检查透明度
console.log('Global alpha:', ctx.globalAlpha)

// 4. 检查是否有裁剪区域
ctx.save()
ctx.restore()

// 5. 检查 Canvas 是否被遮挡
console.log('Canvas display:', getComputedStyle(canvas).display)

图像问题

图像无法加载

问题描述: drawImage() 不显示图像。

原因: 图像未加载完成。

解决方案:

javascript
const img = new Image()
img.onload = function() {
  ctx.drawImage(img, 0, 0)
}
img.onerror = function() {
  console.error('Image failed to load')
}
img.src = 'image.jpg'

跨域图像问题

问题描述: 使用 getImageData()toDataURL() 时报错。

错误信息: SecurityError: The operation is insecure.

解决方案:

javascript
// 方法1:设置 crossOrigin 属性
const img = new Image()
img.crossOrigin = 'anonymous'
img.src = 'https://example.com/image.jpg'

// 服务器需要返回 CORS 头:
// Access-Control-Allow-Origin: *

// 方法2:使用代理服务器
img.src = '/proxy?url=https://example.com/image.jpg'

图像颜色失真

问题描述: 绘制的图像颜色与原图不符。

原因: Canvas 使用预乘 Alpha。

解决方案:

javascript
// 创建不带 Alpha 通道的上下文
const ctx = canvas.getContext('2d', { alpha: false })

大图像处理

问题描述: 大图像加载慢或崩溃。

解决方案:

javascript
// 分块加载大图像
function loadLargeImage(src, chunkSize = 1024) {
  const img = new Image()
  
  img.onload = function() {
    const canvas = document.createElement('canvas')
    const ctx = canvas.getContext('2d')
    
    canvas.width = chunkSize
    canvas.height = chunkSize
    
    for (let y = 0; y < img.height; y += chunkSize) {
      for (let x = 0; x < img.width; x += chunkSize) {
        const w = Math.min(chunkSize, img.width - x)
        const h = Math.min(chunkSize, img.height - y)
        
        ctx.clearRect(0, 0, chunkSize, chunkSize)
        ctx.drawImage(img, x, y, w, h, 0, 0, w, h)
        
        // 处理每个块...
      }
    }
  }
  
  img.src = src
}

性能问题

动画卡顿

问题描述: 动画不流畅,FPS 低。

诊断:

javascript
// FPS 监控
let lastTime = performance.now()
let frames = 0

function measureFPS() {
  frames++
  const currentTime = performance.now()
  
  if (currentTime - lastTime >= 1000) {
    console.log(`FPS: ${frames}`)
    frames = 0
    lastTime = currentTime
  }
  
  requestAnimationFrame(measureFPS)
}

解决方案:

  1. 使用 requestAnimationFrame
  2. 使用离屏 Canvas 缓存
  3. 减少绘制调用
  4. 使用脏矩形渲染
javascript
// 使用脏矩形
const dirtyRects = []

function addDirtyRect(x, y, w, h) {
  dirtyRects.push({ x, y, w, h })
}

function render() {
  dirtyRects.forEach(rect => {
    ctx.clearRect(rect.x, rect.y, rect.w, rect.h)
    // 只重绘脏区域
  })
  dirtyRects.length = 0
}

内存泄漏

问题描述: 页面内存持续增长。

排查:

javascript
// 1. 事件监听器未移除
// ❌ 错误
class Component {
  constructor(canvas) {
    canvas.addEventListener('click', this.onClick)
  }
}

// ✅ 正确
class Component {
  constructor(canvas) {
    this.canvas = canvas
    canvas.addEventListener('click', this.onClick)
  }
  
  destroy() {
    this.canvas.removeEventListener('click', this.onClick)
  }
}

// 2. 动画未停止
// ✅ 停止动画
function destroy() {
  cancelAnimationFrame(animationId)
}

// 3. ImageData 未释放
// ✅ 使用对象池
const imageDataPool = []

大量绘制性能差

问题描述: 绘制大量图形时性能下降。

解决方案:

javascript
// 批量绘制
ctx.beginPath()
for (let i = 0; i < 1000; i++) {
  ctx.moveTo(x[i], y[i])
  ctx.arc(x[i], y[i], radius, 0, Math.PI * 2)
}
ctx.fill()

// 使用 Path2D
const path = new Path2D()
// 添加路径...
ctx.fill(path)

交互问题

点击坐标不准确

问题描述: 点击位置与实际绘制位置不符。

原因: 未考虑 Canvas 边界和缩放。

解决方案:

javascript
function getMousePos(canvas, e) {
  const rect = canvas.getBoundingClientRect()
  const scaleX = canvas.width / rect.width
  const scaleY = canvas.height / rect.height
  
  return {
    x: (e.clientX - rect.left) * scaleX,
    y: (e.clientY - rect.top) * scaleY
  }
}

触摸事件问题

问题描述: 触摸时页面滚动或缩放。

解决方案:

javascript
canvas.addEventListener('touchstart', (e) => {
  e.preventDefault()
  // 处理触摸
}, { passive: false })

canvas.addEventListener('touchmove', (e) => {
  e.preventDefault()
  // 处理移动
}, { passive: false })

图形选择不准确

问题描述: 点击图形时判断错误。

解决方案:

javascript
// 使用 isPointInPath
function isPointInShape(x, y) {
  // 重建路径
  ctx.beginPath()
  ctx.arc(shape.x, shape.y, shape.radius, 0, Math.PI * 2)
  return ctx.isPointInPath(x, y)
}

// 或使用距离计算
function isPointInCircle(px, py, cx, cy, r) {
  const dx = px - cx
  const dy = py - cy
  return dx * dx + dy * dy <= r * r
}

兼容性问题

IE 不支持某些功能

问题描述: 某些 API 在 IE 中不可用。

解决方案:

javascript
// Polyfill
if (!CanvasRenderingContext2D.prototype.roundRect) {
  CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
    if (w < 2 * r) r = w / 2
    if (h < 2 * r) r = h / 2
    this.moveTo(x + r, y)
    this.arcTo(x + w, y, x + w, y + h, r)
    this.arcTo(x + w, y + h, x, y + h, r)
    this.arcTo(x, y + h, x, y, r)
    this.arcTo(x, y, x + w, y, r)
    return this
  }
}

iOS Safari 图像限制

问题描述: 大图像在 iOS Safari 中无法显示。

原因: iOS Safari 有图像大小限制。

解决方案:

javascript
function checkCanvasSize(width, height) {
  const maxPixels = 16777216 // 16MP for iOS
  const pixels = width * height
  
  if (pixels > maxPixels) {
    console.warn('Canvas size exceeds iOS limit')
    // 缩小尺寸
    const scale = Math.sqrt(maxPixels / pixels)
    return {
      width: Math.floor(width * scale),
      height: Math.floor(height * scale)
    }
  }
  
  return { width, height }
}

Firefox 性能问题

问题描述: Firefox 中某些操作性能差。

解决方案:

javascript
// 避免频繁的 getImageData
// 使用缓存
const imageDataCache = new Map()

function getImageDataCached(x, y, w, h) {
  const key = `${x},${y},${w},${h}`
  if (!imageDataCache.has(key)) {
    imageDataCache.set(key, ctx.getImageData(x, y, w, h))
  }
  return imageDataCache.get(key)
}

其他问题

toDataURL 返回空白

问题描述: canvas.toDataURL() 返回空白图像。

原因: Canvas 内容为空或跨域污染。

解决方案:

javascript
// 检查是否有内容
console.log('Canvas width:', canvas.width)
console.log('Canvas height:', canvas.height)

// 确保已绘制内容
ctx.fillStyle = 'red'
ctx.fillRect(0, 0, 10, 10)

// 检查是否跨域污染
try {
  canvas.toDataURL()
} catch (e) {
  console.error('Canvas tainted:', e)
}

save/restore 不生效

问题描述: 状态保存/恢复后仍受影响。

原因: save()restore() 调用不匹配。

解决方案:

javascript
// 使用计数确保匹配
let saveCount = 0

function safeSave() {
  ctx.save()
  saveCount++
}

function safeRestore() {
  if (saveCount > 0) {
    ctx.restore()
    saveCount--
  }
}

// 或使用 try-finally
function withState(callback) {
  ctx.save()
  try {
    callback()
  } finally {
    ctx.restore()
  }
}

渐变颜色不对

问题描述: 渐变颜色与预期不符。

原因: 渐变坐标设置错误。

解决方案:

javascript
// 确保渐变范围覆盖绘制区域
const gradient = ctx.createLinearGradient(
  x, y,           // 起点
  x + width, y    // 终点
)

// 对于圆形渐变
const radialGradient = ctx.createRadialGradient(
  cx, cy, innerRadius,  // 内圆
  cx, cy, outerRadius   // 外圆
)

文字显示问题

问题描述: 文字模糊或样式不正确。

解决方案:

javascript
// 1. 确保字体格式正确
ctx.font = 'bold 20px Arial, sans-serif'

// 2. 处理高 DPI
const dpr = window.devicePixelRatio || 1
ctx.font = `${20 * dpr}px Arial`

// 3. 使用 textBaseline 对齐
ctx.textBaseline = 'top'
ctx.textAlign = 'left'

调试技巧

绘制边界框

javascript
function drawBoundingBox(x, y, width, height) {
  ctx.strokeStyle = 'red'
  ctx.lineWidth = 1
  ctx.strokeRect(x, y, width, height)
}

显示坐标系

javascript
function drawCoordinateSystem() {
  ctx.save()
  ctx.strokeStyle = 'rgba(0, 0, 0, 0.2)'
  ctx.lineWidth = 1
  
  // 绘制网格
  for (let x = 0; x < canvas.width; x += 50) {
    ctx.beginPath()
    ctx.moveTo(x, 0)
    ctx.lineTo(x, canvas.height)
    ctx.stroke()
  }
  
  for (let y = 0; y < canvas.height; y += 50) {
    ctx.beginPath()
    ctx.moveTo(0, y)
    ctx.lineTo(canvas.width, y)
    ctx.stroke()
  }
  
  // 绘制坐标轴
  ctx.strokeStyle = 'red'
  ctx.lineWidth = 2
  ctx.beginPath()
  ctx.moveTo(0, 0)
  ctx.lineTo(canvas.width, 0)
  ctx.moveTo(0, 0)
  ctx.lineTo(0, canvas.height)
  ctx.stroke()
  
  ctx.restore()
}

日志输出

javascript
function logDraw(method, ...args) {
  console.log(`Drawing ${method}:`, args)
  ctx[method](...args)
}

// 使用
logDraw('fillRect', 10, 10, 100, 50)

相关资源


返回Canvas 教程目录 | 上一篇最佳实践