Canvas绘图示例

什么是 HTML Canvas

HTML <canvas> 元素是 HTML5 新增的一个绘图标签,它提供了一个可以通过 JavaScript 绘制图形的区域。Canvas 本质上是一个位图画布,允许开发者通过脚本(通常是 JavaScript)动态渲染图形、图表、动画等内容。

基本语法

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

Canvas 的主要特性

  1. 动态绘图能力:可以在网页上实时绘制各种图形
  2. 像素级操作:可以对画布上的每个像素进行精确控制
  3. 无插件:不需要任何额外插件,原生支持在现代浏览器中
  4. 丰富的 API:提供了丰富的绘图方法和属性

基本使用步骤

  1. 获取 Canvas 元素

    1const canvas = document.getElementById('myCanvas');
  2. 获取绘图上下文

    1const ctx = canvas.getContext('2d');
  3. 开始绘图

    1ctx.fillStyle = 'red';
    2ctx.fillRect(10, 10, 150, 80);

常用绘图方法

绘制矩形

  • fillRect(x, y, width, height) - 绘制填充矩形
  • strokeRect(x, y, width, height) - 绘制矩形边框
  • clearRect(x, y, width, height) - 清除指定矩形区域

绘制路径

  • beginPath() - 开始新路径
  • moveTo(x, y) - 移动画笔到指定位置
  • lineTo(x, y) - 绘制直线到指定位置
  • arc(x, y, radius, startAngle, endAngle, anticlockwise) - 绘制圆弧
  • closePath() - 闭合路径
  • fill() - 填充路径
  • stroke() - 描边路径

文本绘制

  • fillText(text, x, y) - 绘制填充文本
  • strokeText(text, x, y) - 绘制文本轮廓

样式控制

  1. 颜色

    1ctx.fillStyle = 'blue'; // 设置填充颜色
    2ctx.strokeStyle = '#FF0000'; // 设置描边颜色
  2. 线条样式

    1ctx.lineWidth = 5; // 设置线条宽度
    2ctx.lineCap = 'round'; // 设置线条末端样式
    3ctx.lineJoin = 'bevel'; // 设置线条连接处样式

应用场景

  1. 数据可视化:绘制各种图表(柱状图、折线图、饼图等)
  2. 游戏开发:开发2D网页游戏
  3. 图像处理:实现滤镜、裁剪等图像处理功能
  4. 动画效果:创建各种动态效果
  5. 交互式绘图:实现画板、签名板等功能

性能优化建议

  1. 尽量减少不必要的画布重绘
  2. 对于复杂的静态图形,可以考虑先绘制到离屏canvas
  3. 合理使用requestAnimationFrame进行动画绘制
  4. 对于大量相似图形,考虑使用路径批量绘制而非单独绘制

浏览器兼容性

现代浏览器(Chrome、Firefox、Safari、Edge等)都良好支持Canvas。对于IE9以下版本,可以使用兼容库如excanvas.js。

示例:画一个哆啦A梦

1<!DOCTYPE html>
2<html lang="zh">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>Canvas 哆啦A梦</title>
7    <style>
8        body {
9            background: #f0f0f0;
10            display: flex;
11            justify-content: center;
12            align-items: center;
13            min-height: 100vh;
14            margin: 0;
15        }
16        canvas {
17            display: block;
18            border: 1px solid #ccc;
19            background: white;
20            box-shadow: 0 5px 15px rgba(0,0,0,0.2);
21        }
22    </style>
23</head>
24<body>
25    <canvas id="doraemonCanvas" width="400" height="400"></canvas>
26    <script>
27        (function() {
28            const canvas = document.getElementById('doraemonCanvas');
29            const ctx = canvas.getContext('2d');
30
31            // 辅助函数:将 tkinter 的 create_oval(矩形坐标) 转换为 canvas 椭圆
32            function drawOvalFromRect(x1, y1, x2, y2, fill, outline = 'black', lineWidth = 1) {
33                const cx = (x1 + x2) / 2;
34                const cy = (y1 + y2) / 2;
35                const rx = (x2 - x1) / 2;
36                const ry = (y2 - y1) / 2;
37                ctx.beginPath();
38                ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
39                ctx.fillStyle = fill;
40                ctx.fill();
41                if (outline) {
42                    ctx.strokeStyle = outline;
43                    ctx.lineWidth = lineWidth;
44                    ctx.stroke();
45                }
46            }
47
48            // 辅助函数:绘制 tkinter 的 chord 弧(填充扇形)
49            function drawChordFromRect(x1, y1, x2, y2, startAngleDeg, extentDeg, fill, outline = 'black') {
50                const cx = (x1 + x2) / 2;
51                const cy = (y1 + y2) / 2;
52                const rx = (x2 - x1) / 2;
53                const ry = (y2 - y1) / 2;
54                const startRad = startAngleDeg * Math.PI / 180;
55                const endRad = (startAngleDeg + extentDeg) * Math.PI / 180;
56
57                ctx.beginPath();
58                ctx.moveTo(cx, cy);
59                // 计算弧起点 (相对于椭圆)
60                const startX = cx + rx * Math.cos(startRad);
61                const startY = cy + ry * Math.sin(startRad);
62                ctx.lineTo(startX, startY);
63                // 绘制椭圆弧 (使用 ellipse 的一段)
64                // 由于 canvas 没有直接绘制椭圆弧的简单方法,这里用缩放变换模拟
65                // 更简单的做法:保存上下文,缩放,画圆,再恢复
66                ctx.save();
67                ctx.translate(cx, cy);
68                ctx.scale(1, ry / rx);  // 将椭圆变为圆 (rx 方向不变,y 方向缩放)
69                ctx.beginPath();
70                ctx.moveTo(0, 0);
71                ctx.lineTo(Math.cos(startRad) * rx, Math.sin(startRad) * rx * (rx / ry)); // 需要调整
72                // 这种方法太复杂,改用参数方程直接画线
73                ctx.restore();
74
75                // 改用更直接的方法:用很多小线段逼近椭圆弧
76                ctx.beginPath();
77                ctx.moveTo(cx, cy);
78                const steps = 50;
79                for (let i = 0; i <= steps; i++) {
80                    const t = i / steps;
81                    const angle = startRad + t * (endRad - startRad);
82                    const x = cx + rx * Math.cos(angle);
83                    const y = cy + ry * Math.sin(angle);
84                    if (i === 0) {
85                        ctx.lineTo(x, y);
86                    } else {
87                        ctx.lineTo(x, y);
88                    }
89                }
90                ctx.closePath();
91                ctx.fillStyle = fill;
92                ctx.fill();
93                if (outline) {
94                    ctx.strokeStyle = outline;
95                    ctx.lineWidth = 1;
96                    ctx.stroke();
97                }
98            }
99
100            // 清除画布
101            ctx.clearRect(0, 0, 400, 400);
102
103            // 大蓝脸 (圆脸)
104            drawOvalFromRect(125, 70, 275, 220, 'blue', 'blue', 1);
105
106            // 白色脸
107            drawOvalFromRect(140, 100, 260, 220, 'white', 'black', 1);
108
109            // 眼睛白色部分 (左眼和右眼)
110            drawOvalFromRect(200, 80, 230, 120, 'white', 'black', 1);   // 右眼 (从角色视角)
111            drawOvalFromRect(170, 80, 200, 120, 'white', 'black', 1);   // 左眼
112
113            // 黑色眼珠
114            drawOvalFromRect(203, 92, 215, 108, 'black', 'black', 1);   // 右眼珠
115            drawOvalFromRect(185, 92, 197, 108, 'black', 'black', 1);   // 左眼珠
116
117            // 眼睛高光
118            drawOvalFromRect(206, 95, 212, 105, 'white', null);          // 右高光 (无边框)
119            drawOvalFromRect(188, 95, 194, 105, 'white', null);          // 左高光
120
121            // 红色鼻子
122            drawOvalFromRect(193, 115, 207, 130, 'red', 'black', 1);
123
124            // 嘴巴弧线 (arc)
125            ctx.beginPath();
126            ctx.arc(200, 120, 60, 60 * Math.PI / 180, 120 * Math.PI / 180);
127            ctx.strokeStyle = 'black';
128            ctx.lineWidth = 1;
129            ctx.stroke();
130
131            // 鼻子下的竖线
132            ctx.beginPath();
133            ctx.moveTo(200, 130);
134            ctx.lineTo(200, 180);
135            ctx.strokeStyle = 'black';
136            ctx.stroke();
137
138            // 胡须
139            ctx.beginPath();
140            // 上横须
141            ctx.moveTo(215, 150); ctx.lineTo(245, 150);
142            ctx.moveTo(155, 150); ctx.lineTo(185, 150);
143            // 斜须
144            ctx.moveTo(158, 127); ctx.lineTo(185, 137);
145            ctx.moveTo(215, 137); ctx.lineTo(242, 127);
146            ctx.moveTo(158, 170); ctx.lineTo(185, 163);
147            ctx.moveTo(215, 163); ctx.lineTo(242, 168);
148            ctx.strokeStyle = 'black';
149            ctx.stroke();
150
151            // 身体矩形 (蓝色)
152            ctx.fillStyle = 'blue';
153            ctx.strokeStyle = 'blue';
154            ctx.lineWidth = 1;
155            ctx.fillRect(150, 200, 100, 85);   // 150->250, 200->285
156            ctx.strokeRect(150, 200, 100, 85);
157
158            // 白色肚皮 (chord 弧)  (160,190,240,270) start=135 extent=270
159            // 使用自定义函数绘制扇形填充
160            // 为了简化,我们使用路径手动绘制扇形
161            const cx1 = (160 + 240) / 2; // 200
162            const cy1 = (190 + 270) / 2; // 230
163            const rx1 = (240 - 160) / 2; // 40
164            const ry1 = (270 - 190) / 2; // 40
165            const start1 = -45 * Math.PI / 180;
166            const end1 = (-45 + 270) * Math.PI / 180; // 405° 相当于 45°
167            ctx.beginPath();
168            ctx.moveTo(cx1, cy1);
169            const steps = 50;
170            for (let i = 0; i <= steps; i++) {
171                const t = i / steps;
172                const angle = start1 + t * (end1 - start1);
173                const x = cx1 + rx1 * Math.cos(angle);
174                const y = cy1 + ry1 * Math.sin(angle);
175                ctx.lineTo(x, y);
176            }
177            ctx.closePath();
178            ctx.fillStyle = 'white';
179            ctx.fill();
180            ctx.strokeStyle = 'black';
181            ctx.stroke();
182
183            // 计算弧的起点 (start1 = -45° 对应 315°) 和终点 (end1 = 225°)
184            const startX = cx1 + rx1 * Math.cos(start1);
185            const startY = cy1 + ry1 * Math.sin(start1);
186            const endX = cx1 + rx1 * Math.cos(end1);
187            const endY = cy1 + ry1 * Math.sin(end1);
188
189            // 计算椭圆上顶点 (角度 90°)
190            const topX = cx1;  // 90° 的余弦为0,所以 x = cx1
191            const topY = cy1;  // 90° 的正弦为1,所以 y = cy1 + ry1
192
193            // 绘制填充三角形
194            ctx.beginPath();
195            ctx.moveTo(startX, startY);
196            ctx.lineTo(endX, endY);
197            ctx.lineTo(topX, topY);
198            ctx.closePath();
199            ctx.fillStyle = 'white';   // 与扇形颜色一致
200            ctx.fill();
201            ctx.strokeStyle = 'white'; // 可选:添加黑色边框以保持风格
202            ctx.stroke();
203
204            // 白色小弧线 (185,270,215,300) start=0 extent=180
205            const cx2 = (185 + 215) / 2; // 200
206            const cy2 = (270 + 300) / 2; // 285
207            const rx2 = (215 - 185) / 2; // 15
208            const ry2 = (300 - 270) / 2; // 15
209            const start2 = 180 * Math.PI / 180;
210            const end2 = 360 * Math.PI / 180;
211            ctx.beginPath();
212            ctx.moveTo(cx2, cy2);
213            for (let i = 0; i <= steps; i++) {
214                const t = i / steps;
215                const angle = start2 + t * (end2 - start2);
216                const x = cx2 + rx2 * Math.cos(angle);
217                const y = cy2 + ry2 * Math.sin(angle);
218                ctx.lineTo(x, y);
219            }
220            ctx.closePath();
221            ctx.fillStyle = 'white';
222            ctx.fill();
223            ctx.strokeStyle = 'white';
224            ctx.stroke();
225
226
227            // 两只脚 (白色椭圆)
228            drawOvalFromRect(140, 275, 190, 295, 'white', 'black', 1);
229            drawOvalFromRect(210, 275, 260, 295, 'white', 'black', 1);
230
231            // 手臂多边形 (左手)
232            ctx.beginPath();
233            ctx.moveTo(150, 205);
234            ctx.lineTo(150, 235);
235            ctx.lineTo(120, 250);
236            ctx.lineTo(120, 235);
237            ctx.closePath();
238            ctx.fillStyle = 'blue';
239            ctx.fill();
240            ctx.strokeStyle = 'blue';
241            ctx.stroke();
242
243            // 右手
244            ctx.beginPath();
245            ctx.moveTo(250, 205);
246            ctx.lineTo(250, 235);
247            ctx.lineTo(280, 250);
248            ctx.lineTo(280, 235);
249            ctx.closePath();
250            ctx.fillStyle = 'blue';
251            ctx.fill();
252            ctx.strokeStyle = 'blue';
253            ctx.stroke();
254
255            // 手掌圆形
256            drawOvalFromRect(110, 230, 135, 255, 'white', 'black', 1);
257            drawOvalFromRect(265, 230, 290, 255, 'white', 'black', 1);
258
259            // 红色项圈 (粗线)
260            ctx.beginPath();
261            ctx.moveTo(150, 200);
262            ctx.lineTo(250, 200);
263            ctx.strokeStyle = 'red';
264            ctx.lineWidth = 10;
265            ctx.lineCap = 'round';
266            ctx.stroke();
267
268            // 铃铛
269            // 黄色主体
270            drawOvalFromRect(190, 200, 210, 220, 'yellow', 'black', 1);
271            // 黑色粗线
272            ctx.beginPath();
273            ctx.moveTo(191, 210);
274            ctx.lineTo(209, 210);
275            ctx.strokeStyle = 'black';
276            ctx.lineWidth = 5;
277            ctx.lineCap = 'round';
278            ctx.stroke();
279            // 黄色细线
280            ctx.beginPath();
281            ctx.moveTo(192, 210);
282            ctx.lineTo(208, 210);
283            ctx.strokeStyle = 'yellow';
284            ctx.lineWidth = 3;
285            ctx.lineCap = 'round';
286            ctx.stroke();
287            // 小红点
288            drawOvalFromRect(198, 213, 202, 217, 'red', null);
289            // 小红点下的竖线
290            ctx.beginPath();
291            ctx.moveTo(200, 218);
292            ctx.lineTo(200, 220);
293            ctx.strokeStyle = 'black';
294            ctx.lineWidth = 1;
295            ctx.stroke();
296
297            // 口袋弧线 (170,200,230,260) start=180 extent=180
298            const cx3 = (170 + 230) / 2; // 200
299            const cy3 = (200 + 260) / 2; // 230
300            const rx3 = (230 - 170) / 2; // 30
301            const ry3 = (260 - 200) / 2; // 30
302            const start3 = 0 * Math.PI / 180;
303            const end3 = 180 * Math.PI / 180; // 180+180=360
304            ctx.beginPath();
305            ctx.moveTo(cx3, cy3);
306            for (let i = 0; i <= steps; i++) {
307                const t = i / steps;
308                const angle = start3 + t * (end3 - start3);
309                const x = cx3 + rx3 * Math.cos(angle);
310                const y = cy3 + ry3 * Math.sin(angle);
311                ctx.lineTo(x, y);
312            }
313            ctx.closePath();
314            ctx.fillStyle = 'white';
315            ctx.fill();
316            ctx.strokeStyle = 'black';
317            ctx.stroke();
318
319            // 恢复线宽为默认,以免影响后续 (虽然没有了)
320            ctx.lineWidth = 1;
321        })();
322    </script>
323</body>
324</html>