重学canvas 2D

188 阅读8分钟

image.png

<canvas>是HTML5新增的元素,它可以用来绘制图形数据。这个元素本身只是一个容器,实际的绘制需要使用JavaScript。<canvas>的优势在于它提供了丰富的绘图功能,包括绘制形状、渐变、图像处理、动画等。

<canvas> API

<canvas>提供了一个2D绘图上下文(通过getContext('2d')方法获取)和一个用于WebGL的3D绘图上下文(通过getContext('webgl')方法获取)。以下是一些常用的2D绘图API:

  • fillRect(x, y, width, height): 绘制一个填充的矩形。
  • strokeRect(x, y, width, height): 绘制一个矩形的边框。
  • clearRect(x, y, width, height): 清除指定的矩形区域内容。
  • fillText(text, x, y, [maxWidth]): 在指定位置填充文本。
  • strokeText(text, x, y, [maxWidth]): 在指定位置绘制文本的边框。
  • beginPath(): 开始一条路径,或重置当前的路径。
  • moveTo(x, y): 将笔触移动到指定的坐标点。
  • lineTo(x, y): 绘制一条从当前位置到指定x以及y位置的直线。
  • arc(x, y, radius, startAngle, endAngle, anticlockwise): 绘制一个圆弧。
  • fill(): 填充当前绘图的路径。
  • stroke(): 绘制当前路径的边框。
  • closePath(): 创建从当前点到开始点的路径。
  • setTransform(scaleX, skewX, skewY, scaleY, translateX, translateY): 通过直接构造一个变换矩阵来设置当前变换。

事件处理

<canvas>元素本身并不具备事件处理的API,但是它可以响应鼠标事件、触摸事件和键盘事件,如下所示:

var canvas = document.getElementById('myCanvas');
canvas.addEventListener('mousedown', function(event) {
    // 处理鼠标按下事件
});

canvas.addEventListener('mousemove', function(event) {
    // 处理鼠标移动事件
});

canvas.addEventListener('mouseup', function(event) {
    // 处理鼠标松开事件
});

使用

在HTML中,先声明一个<canvas>元素:

<canvas id="myCanvas" width="200" height="200"></canvas>

然后在JavaScript中获取这个元素并开始绘制:

var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

// 绘制一个红色的矩形
ctx.fillStyle = 'red';
ctx.fillRect(10, 10, 100, 100);

应用场景

绘制时钟

以下是一个简单的时钟

function drawClock() {
  var now = new Date();
  var ctx = document.getElementById('myCanvas').getContext('2d');
  ctx.save();
  ctx.clearRect(0, 0, 150, 150);
  ctx.translate(75, 75);
  ctx.scale(0.4, 0.4);
  ctx.rotate(-Math.PI / 2);
  ctx.strokeStyle = "black";
  ctx.fillStyle = "white";
  ctx.lineWidth = 8;
  ctx.lineCap = "round";

  // 绘制时针、分针和秒针
  ctx.save();
  ctx.rotate((Math.PI / 6) * now.getHours() + (Math.PI / 360) * now.getMinutes() + (Math.PI / 21600) * now.getSeconds());
  ctx.lineWidth = 14;
  ctx.beginPath();
  ctx.moveTo(-20, 0);
  ctx.lineTo(80, 0);
  ctx.stroke();
  ctx.restore();

  // ... 其他绘制代码 ...

  ctx.restore();
}

setInterval(drawClock, 1000);

电子签名

实现一个简单的电子签名板

<canvas id="signature-pad" width="300" height="150"></canvas>
var canvas = document.getElementById('signature-pad');
var ctx = canvas.getContext('2d');

// 设置画笔属性
ctx.strokeStyle = '#000000';
ctx.lineWidth = 2;

// 监听鼠标事件
canvas.onmousedown = function(e) {
  ctx.beginPath();
  ctx.moveTo(e.clientX - canvas.offsetLeft, e.clientY - canvas.offsetTop);
  
  document.onmousemove = function(e) {
    ctx.lineTo(e.clientX - canvas.offsetLeft, e.clientY - canvas.offsetTop);
    ctx.stroke();
  };
  
  document.onmouseup = function() {
    document.onmousemove = null;
    document.onmouseup = null;
  };
};

游戏绘制

<canvas>常用于2D游戏的绘制,如平台游戏、拼图游戏等。

// 假设有个游戏循环函数gameLoop
function gameLoop() {
  // 清除画布
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  // 绘制游戏元素
  // ...

  // 更新游戏状态
  // ...

  requestAnimationFrame(gameLoop);
}

gameLoop();

数据可视化

<canvas>也可以用于数据可视化,如绘制图表和图形。

// 绘制一个简单的柱状图
var values = [100, 200, 300, 400, 500];
var max = Math.max(...values);

values.forEach(function(value, index) {
  var height = (value / max) * canvas.height;
  ctx.fillRect(index * 50, canvas.height - height, 40, height);
});

图像处理

<canvas>可以用来对图像进行处理,例如应用滤镜、调整亮度或对图像进行裁剪。

var img = new Image();
img.onload = function() {
  ctx.drawImage(img, 0, 0);
  ctx.globalAlpha = 0.5; // 设置透明度
  // 其他图像处理逻辑...
};
img.src = 'image.jpg';

动画制作

可以通过连续变化<canvas>上的图像来创建动画效果。

function animate() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  // 绘制动画帧
  // ...
  requestAnimationFrame(animate);
}
animate();

物理模拟

<canvas>可以用于模拟物理现象,如粒子系统、流体动力学等。

// 粒子系统的基础示例
function Particle(x, y) {
  this.x = x;
  this.y = y;
  // 其他属性和方法...
}

var particles = [new Particle(50, 50), new Particle(100, 100)];
// 在动画循环中更新粒子状态

教育应用

利用<canvas>来创建各种教育工具,如几何图形绘制、函数图像绘制等。

// 绘制一个坐标系
function drawAxes() {
  ctx.beginPath();
  ctx.moveTo(0, canvas.height / 2);
  ctx.lineTo(canvas.width, canvas.height / 2);
  ctx.moveTo(canvas.width / 2, 0);
  ctx.lineTo(canvas.width / 2, canvas.height);
  ctx.stroke();
}
drawAxes();

基于Web的设计工具

<canvas>可以用来制作设计工具,如颜色选取器、图形编辑器等。

// 基本的图形编辑功能示例
var isDragging = false;
canvas.onmousedown = function(e) {
  isDragging = true;
};
canvas.onmousemove = function(e) {
  if (isDragging) {
    // 更新图形位置
  }
};
canvas.onmouseup = function(e) {
  isDragging = false;
};

自定义控件

使用<canvas>来绘制自定义的UI控件,如滑块、按钮等。

// 绘制一个自定义滑块
function drawSlider() {
  ctx.fillRect(10, 10, 100, 5);
  ctx.beginPath();
  ctx.arc(60, 12.5, 7.5, 0, Math.PI * 2);
  ctx.fill();
}
drawSlider();

常见问题解决

解决模糊问题

<canvas>在高分辨率屏幕上可能会出现模糊问题,这通常是由于设备像素比(device pixel ratio)引起的。解决方法是使用设备的实际像素比来调整画布大小:

var dpr = window.devicePixelRatio || 1;
var rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
var ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);

解决1px问题

当绘制1px线条时,由于像素对齐问题,线条可能会显示得不够清晰。解决方法是对坐标进行0.5像素的偏移:

ctx.beginPath();
ctx.moveTo(x + 0.5, y);
ctx.lineTo(x + 0.5, y + height);
ctx.stroke();

动态调整画布大小

如果需要根据窗口大小变化动态调整画布大小,可以监听窗口的resize事件:

window.addEventListener('resize', resizeCanvas, false);

function resizeCanvas() {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
  // 重新绘制画布内容
}
resizeCanvas();

性能优化

性能优化是使用 HTML <canvas> 进行复杂图形处理时的一个关键考虑点。

减少绘制操作

每次调用 <canvas> 的绘制方法时,都会产生性能开销。优化的一个方法是尽可能减少绘制操作的次数:

  • 使用图层: 对于不经常改变的内容,可以将它们绘制到一个离屏 <canvas> 上,然后将这个 <canvas> 作为图像绘制到屏幕上。
  • 合并绘制调用: 如果你需要绘制多个相同的对象,可以通过合并它们的绘制调用来减少开销,例如使用 fillRect() 绘制多个矩形时,尝试合并为一个更大的 fillRect() 调用。
// 创建一个离屏 canvas
var offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = 100;
offscreenCanvas.height = 100;
var offCtx = offscreenCanvas.getContext('2d');

// 在离屏 canvas 上绘制,例如一个不经常变动的背景
offCtx.fillStyle = 'blue';
offCtx.fillRect(0, 0, 100, 100);

// 在主 canvas 上绘制离屏 canvas 的内容
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

function draw() {
  // 直接将预绘制的内容绘制到屏幕上
  ctx.drawImage(offscreenCanvas, 0, 0);
  requestAnimationFrame(draw);
}

draw();

避免不必要的计算

<canvas> 的绘制循环中,应尽量减少计算量:

  • 预计算: 对于可以预先计算的值,应在循环外计算。
  • 缓存计算结果: 对于在多个绘制周期内不变的计算结果,应该缓存起来,而不是每次绘制时都重新计算。
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
var expensiveComputationResult = expensiveComputation();

function draw() {
  // 使用预先计算并缓存的结果
  ctx.fillStyle = expensiveComputationResult;
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  requestAnimationFrame(draw);
}

function expensiveComputation() {
  // 进行一些复杂计算,并返回结果
  return '#009688'; // 示例:计算得到的颜色
}

draw();

利用现代浏览器的硬件加速

现代浏览器通常提供硬件加速来优化 <canvas> 的性能:

  • 使用 WebGL: 当可用时,使用 WebGL 而不是 2D 上下文可以利用GPU加速。
  • 转换优化: 使用 CSS3 转换(如 translate3d)而不是 <canvas> API 可以触发 GPU 加速。
<style>
  #myCanvas {
    transform: translate3d(0, 0, 0); /* 触发硬件加速 */
  }
</style>
<canvas id="myCanvas"></canvas>

优化动画

动画是 <canvas> 中常见的性能瓶颈。以下是一些优化技巧:

  • 使用 requestAnimationFrame: 这个 API 能够让浏览器优化动画的性能,减少闪烁和卡顿现象。
  • 限制帧率: 如果动画不需要非常平滑,可以限制帧率以减轻CPU或GPU的负担。
  • 减少每帧工作量: 分析和减少每一帧需要完成的工作量,例如通过空间分割技术只更新视图中变化的部分。
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
var lastFrameTime = Date.now();

function animate() {
  var now = Date.now();
  var elapsed = now - lastFrameTime;
  
  if (elapsed > (1000 / 60)) { // 约等于每秒60帧
    lastFrameTime = now - (elapsed % (1000 / 60));
    // 更新动画状态
    // ...
    // 重绘画布
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    // 绘制动画
    // ...
  }
  requestAnimationFrame(animate);
}

animate();

管理资源

图像和其他媒体资源可以增加 <canvas> 应用的内存使用和加载时间:

  • 优化图像: 使用压缩的图像格式,并且只加载需要显示的尺寸。
  • 懒加载: 只在需要时加载资源,不在页面加载时就加载所有资源。
  • 缓存: 重用图像和模式,避免重复加载或生成。
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
var image = new Image();

// 懒加载图像
image.onload = function() {
  // 图像加载完成后,绘制到 canvas 上
  ctx.drawImage(image, 0, 0);
};
// 仅在需要显示图像时设置 src
image.src = 'optimized-image.jpg'; // 已优化的图像路径

监控性能

使用浏览器的开发者工具来监控 <canvas> 应用的性能:

  • 分析绘制时间: 使用性能分析工具来确定哪些绘制操作最耗时。
  • 内存管理: 监控内存使用情况,及时释放不再需要的资源。
// 假设我们有一个复杂的绘制函数
function complexDrawingOperation() {
  // 复杂的绘制代码...
}

// 使用console.time和console.timeEnd来测量绘制时间
console.time('complexDrawing');
complexDrawingOperation();
console.timeEnd('complexDrawing');

结论

HTML <canvas> 元素是前端开发中的一项强大工具,它为复杂的图形处理和交互式应用提供了无限的可能性。通过理解其API、事件处理以及探索其丰富的应用场景,开发者可以利用<canvas>来创建各种精彩的Web体验。记得在开发过程中考虑响应式设计和高DPI设备的特殊需求,以确保你的<canvas>应用能在不同环境下都提供一致的用户体验。