1.6 Canvas:2D图形绘制与动画基础

Canvas是HTML5引入的绘图API,通过<canvas>标签创建一个可通过JavaScript动态绘制图形的区域,支持2D图形、文本、图像合成及简单动画,广泛应用于数据可视化、游戏开发、图表绘制等场景。

Canvas核心概念

  • 绘图上下文(Context)<canvas>本身只是一个矩形画布容器,实际绘图操作需通过其2D渲染上下文对象(CanvasRenderingContext2D)完成。通过getContext('2d')方法获取上下文对象,后续所有绘图API均通过该对象调用。
  • 坐标系统:默认以画布左上角为原点(0,0),X轴向右为正方向,Y轴向下为正方向,单位为像素(px)。可通过translate()scale()等方法变换坐标系统。
  • 绘制状态:上下文对象包含当前绘制状态(如颜色、线条宽度、字体等),可通过save()保存当前状态,restore()恢复之前保存的状态,实现复杂绘图逻辑。

基础绘图API分类

  • 路径绘制:通过定义点、线、曲线组成的路径来绘制形状,核心方法包括:
  • beginPath():开始新路径(重置当前路径)。
  • moveTo(x,y):将画笔移动到指定坐标(不绘制线条)。
  • lineTo(x,y):从当前位置绘制直线到目标坐标。
  • arc(x,y,radius,startAngle,endAngle,anticlockwise):绘制圆弧/圆形(anticlockwisetrue时逆时针绘制)。
  • closePath():闭合路径(连接当前点与起始点)。
  • stroke():描边路径(绘制线条)。
  • fill():填充路径内部区域。
  • 矩形绘制:提供专用方法快速绘制矩形,无需手动定义路径:
  • strokeRect(x,y,width,height):描边矩形。
  • fillRect(x,y,width,height):填充矩形。
  • clearRect(x,y,width,height):清除指定矩形区域(变为透明)。
  • 文本绘制:支持填充文本与描边文本:
  • fillText(text,x,y,maxWidth):填充文本(maxWidth可选,限制文本最大宽度)。
  • strokeText(text,x,y,maxWidth):描边文本。
  • font:设置字体样式(如"20px Arial")。
  • textAlign:文本对齐方式(start/end/left/right/center)。
  • 图像绘制:在画布上绘制外部图片:
  • drawImage(image,x,y):绘制图片(image可为<img>元素或Image对象)。
  • drawImage(image,x,y,width,height):缩放绘制图片。
  • drawImage(image,sx,sy,sWidth,sHeight,dx,dy,dWidth,dHeight):裁剪图片并绘制指定区域。

案例1-6:Canvas绘制动态时钟

以下代码使用Canvas实现一个实时更新的模拟时钟,包含表盘、刻度、指针及数字显示:

html
复制代码
<!-- Canvas画布:设置宽度和高度(建议直接在标签上定义,避免CSS拉伸导致模糊) -->
<canvas id="clockCanvas" width="400" height="400" style="border: 2px solid #333;"></canvas>

<script>
// 获取Canvas元素及2D上下文
const canvas = document.getElementById('clockCanvas');
const ctx = canvas.getContext('2d');
// 获取画布中心点坐标(画布宽高的一半)
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
// 时钟半径(画布尺寸的45%,留出边框空间)
const radius = canvas.width * 0.45;

// 定义绘制时钟的函数
function drawClock() {
// 1. 清除画布(每次绘制前清除上一帧内容)
ctx.clearRect(0, 0, canvas.width, canvas.height);

// 2. 绘制表盘背景
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); // 绘制圆形表盘
ctx.fillStyle = '#fff'; // 填充白色
ctx.fill();
ctx.strokeStyle = '#333'; // 描边颜色
ctx.lineWidth = 5; // 描边宽度
ctx.stroke();

// 3. 绘制中心圆点
ctx.beginPath();
ctx.arc(centerX, centerY, 8, 0, Math.PI * 2);
ctx.fillStyle = '#ff0000'; // 红色中心点
ctx.fill();

// 4. 绘制刻度(小时刻度与分钟刻度)
ctx.lineWidth = 2;
for (let i = 0; i < 60; i++) {
// 计算刻度角度(6度/分钟,360度/60分钟)
const angle = (i * Math.PI / 30) - Math.PI / 2; // 减去90度使0分对准顶部
// 刻度起始点(靠近表盘边缘)
const startX = centerX + Math.cos(angle) * (radius - 15);
const startY = centerY + Math.sin(angle) * (radius - 15);
// 刻度结束点(长度根据小时/分钟刻度区分)
let endX, endY;
if (i % 5 === 0) { // 小时刻度(每5分钟一个)
ctx.lineWidth = 4; // 更粗的线条
endX = centerX + Math.cos(angle) * (radius - 30); // 更长的刻度
endY = centerY + Math.sin(angle) * (radius - 30);
// 绘制小时数字(1-12)
const hourNum = i === 0 ? 12 : i / 5; // 0分对应12点
ctx.font = 'bold 20px Arial'; // 字体样式
ctx.textAlign = 'center'; // 文本居中对齐
ctx.textBaseline = 'middle'; // 文本基线居中
ctx.fillStyle = '#333';
// 数字位置(比小时刻度更靠外)
const numX = centerX + Math.cos(angle) * (radius - 50);
const numY = centerY + Math.sin(angle) * (radius - 50);
ctx.fillText(hourNum, numX, numY);
} else { // 分钟刻度
ctx.lineWidth = 2;
endX = centerX + Math.cos(angle) * (radius - 20); // 较短的刻度
endY = centerY + Math.sin(angle) * (radius - 20);
}
// 绘制刻度线
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(endX, endY);
ctx.strokeStyle = '#666';
ctx.stroke();
}

// 5. 获取当前时间
const now = new Date();
const hours = now.getHours() % 12; // 转换为12小时制
const minutes = now.getMinutes();
const seconds = now.getSeconds();
const milliseconds = now.getMilliseconds();

// 6. 绘制时针
const hourAngle = (hours * Math.PI / 6) + (minutes * Math.PI / 360) + (seconds * Math.PI / 21600); // 考虑分钟和秒的偏移
drawHand(hourAngle, radius * 0.5, 6, '#333'); // 长度为半径50%,宽度6px,黑色

// 7. 绘制分针
const minuteAngle = (minutes * Math.PI / 30) + (seconds * Math.PI / 1800); // 考虑秒的偏移
drawHand(minuteAngle, radius * 0.7, 4, '#555'); // 长度为半径70%,宽度4px,深灰色

// 8. 绘制秒针
const secondAngle = (seconds * Math.PI / 30) + (milliseconds * Math.PI / 18000); // 考虑毫秒的偏移
drawHand(secondAngle, radius * 0.85, 2, '#ff0000'); // 长度为半径85%,宽度2px,红色
}

// 辅助函数:绘制指针
function drawHand(angle, length, width, color) {
ctx.beginPath();
ctx.moveTo(centerX, centerY); // 从中心点开始
// 计算指针终点坐标(角度减去90度使0度对准顶部)
const endX = centerX + Math.cos(angle - Math.PI / 2) * length;
const endY = centerY + Math.sin(angle - Math.PI / 2) * length;
ctx.lineTo(endX, endY);
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.lineCap = 'round'; // 指针末端为圆形
ctx.stroke();
}

// 初始绘制一次
drawClock();
// 每秒更新一次(1000毫秒)
setInterval(drawClock, 1000);
</script>

性能注意事项

  • Canvas绘制是"即时模式"(Immediate Mode),每次重绘需手动清除画布(clearRect)并重新绘制所有内容,复杂场景下可能导致性能问题。
  • 对于静态图形,可将绘制结果保存为图片(通过toDataURL()方法),避免重复绘制;对于动画,优先使用requestAnimationFrame()替代setInterval(),使动画帧率与浏览器刷新同步,减少卡顿。