从零开始学 Canvas,用三个小练习搞懂核心 API,再抬头看一眼 3D 世界。
一、Canvas 到底是什么?
Canvas(画布)是 HTML5 提供的一个标签,但它跟普通标签不太一样。普通 <div> 里放的是 DOM 元素,Canvas 里放的却是像素。
<canvas id="myCanvas" width="600" height="400">
<p>你的浏览器不支持 canvas</p>
</canvas>
它有两层身份:
| 身份 | 说明 |
|---|---|
| DOM 树成员 | 一个普通的 HTML 节点,可以被 CSS 控制大小、边框、定位,可以用 querySelector 选中 |
| 独立像素渲染区 | 用 JS 画上去的图形全部变成像素,不在 DOM 树里。F12 审查元素只能看到 <canvas> 标签本身,看不到里面的红方块 |
flowchart TB
subgraph 浏览器
subgraph "DOM 树"
DIV["<div>"]
CANVAS[" ← 标签本身在这里"]
P["<p>"]
end
subgraph "Canvas 渲染区(像素)"
PX["fillRect 画的红方块 ← 只有像素,没有 DOM 节点"]
end
end
CANVAS -.->|"getContext('2d')"| PX
核心认知:
<canvas>是画板,JS 是画笔。两者分开正是前端"三权分立"的体现——HTML 管结构,CSS 管样式,JS 管行为。
二、入门三板斧
每个 Canvas 程序的启动代码就三行:
// ① 拿到 canvas 元素
const canvas = document.querySelector('#myCanvas');
// ② 获取 2D 绘图上下文("画笔")
const ctx = canvas.getContext('2d');
// ③ 开始画
ctx.fillStyle = 'red';
ctx.fillRect(10, 10, 100, 100);
getContext('2d') 返回的是 CanvasRenderingContext2D 对象,所有绘制方法都长在它身上,这是 Canvas 编程的唯一入口。
三、核心 API 分类
3.1 绘制方法:带 Rect 的 vs 不带 Rect 的
这是入门时最容易困惑的地方。为什么有时写 fillRect(),有时写 fill()?
| 类型 | 方法 | 用法 |
|---|---|---|
| Rect 系列(快捷方式) | fillRect(x, y, w, h) | 一步到位,直接画填充矩形 |
strokeRect(x, y, w, h) | 一步到位,直接画描边矩形 | |
clearRect(x, y, w, h) | 擦除指定矩形区域 | |
| 通用系列(万能方式) | fill() | 填充任意形状,需先用路径画好 |
stroke() | 描任意形状,需先用路径画好 |
对比理解:
// Rect 系列:一句话搞定一个矩形
ctx.fillRect(10, 10, 100, 100);
// 通用系列:先画路径,再填充。什么形状都能画
ctx.beginPath();
ctx.arc(200, 200, 50, 0, Math.PI * 2); // 画圆
ctx.fill(); // 填充
fillRect只能画矩形;beginPath + fill()可以画圆、线、贝塞尔曲线……任何形状。
所有坐标参数的规则都一样:(左上角 x, 左上角 y, 宽度, 高度)。
3.2 样式属性:fillStyle vs strokeStyle
这也是初学的迷惑点——长得像,作用不同:
fillStyle = 'red' strokeStyle = 'blue'
(填充色 / 涂满) (描边色 / 勾线)
┌─────────────┐ ┌─────────────┐
│ ██████████ │ │ ┌─────────┐ │
│ ██████████ │ │ │ 空心 │ │
│ ██████████ │ │ └─────────┘ │
└─────────────┘ └─────────────┘
整块都是红色 只有边框是蓝色
fillStyle→ 图形内部的颜色,配合fillRect()/fill()使用strokeStyle→ 图形边框的颜色,配合strokeRect()/stroke()使用lineWidth→ 边框粗细,只对 stroke 生效
// 纯填充:红色实心方块
ctx.fillStyle = 'red';
ctx.fillRect(10, 10, 100, 80);
// 纯描边:蓝色空心框
ctx.strokeStyle = 'blue';
ctx.lineWidth = 4;
ctx.strokeRect(150, 10, 100, 80);
// 填充 + 描边叠加
ctx.fillStyle = 'red';
ctx.strokeStyle = 'blue';
ctx.lineWidth = 4;
ctx.fillRect(300, 10, 100, 80);
ctx.strokeRect(300, 10, 100, 80);
一句话:
fillStyle= 面(涂什么色),strokeStyle= 线(边框什么色)。
配图总结:
flowchart LR
subgraph "样式属性(画之前设置)"
FS["fillStyle<br/>填充色"]
SS["strokeStyle<br/>描边色"]
LW["lineWidth<br/>线宽"]
end
subgraph "绘制方法"
FR["fillRect()<br/>画实心矩形"]
SR["strokeRect()<br/>画空心矩形"]
CR["clearRect()<br/>擦除"]
FILL["fill()<br/>填充路径"]
STROKE["stroke()<br/>描边路径"]
end
FS --> FR
FS --> FILL
SS --> SR
SS --> STROKE
LW --> SR
LW --> STROKE
四、路径绘制 — 画圆和笑脸
用上面的通用系列,可以画任意形状。核心模式:
ctx.beginPath(); // ① 起笔(告诉 canvas "我要开始画一个新形状了")
ctx.arc(x, y, r, 0, 2π); // ② 画弧(圆 = 0 到 2π 的弧)
ctx.fillStyle = 'yellow';
ctx.fill(); // ③ 填充
画一个笑脸:
// 脸(大黄圆)
ctx.beginPath();
ctx.arc(200, 200, 100, 0, Math.PI * 2);
ctx.fillStyle = 'yellow';
ctx.fill();
// 左眼(小黑圆)
ctx.fillStyle = 'black';
ctx.beginPath();
ctx.arc(160, 160, 10, 0, Math.PI * 2);
ctx.fill();
// 右眼
ctx.beginPath();
ctx.arc(240, 160, 10, 0, Math.PI * 2);
ctx.fill();
// 嘴巴(半圆弧)
ctx.beginPath();
ctx.arc(200, 220, 40, 0, Math.PI); // 只画 π,就是半圆(微笑弧)
ctx.stroke();
规律:每个独立图形都要
beginPath()起笔 +fill()或stroke()收笔。arc()的参数是(圆心x, 圆心y, 半径, 起始角度, 结束角度),角度用弧度制。
五、动画 — requestAnimationFrame
Canvas 做动画和游戏的核心就是帧循环。
为什么不用 setInterval?
显示器以固定频率刷新(通常 60Hz,即每秒 60 帧),setInterval 的时间间隔和屏幕刷新率不同步,会导致掉帧或画面撕裂。requestAnimationFrame 是浏览器专为动画设计的 API,自动对齐屏幕刷新率,画面更流畅。
四步动画循环
function animate() {
// ① 擦掉上一帧
ctx.clearRect(0, 0, canvas.width, canvas.height);
// ② 画新的帧
ctx.fillStyle = '#4299e1';
ctx.fillRect(x, y, width, height);
// ③ 更新状态(移动位置)
x += speed;
if (x > canvas.width) {
x = -width; // 超出右边界 → 从左边重新进入
}
// ④ 请求下一帧(递归)
requestAnimationFrame(animate);
}
animate(); // 启动循环
flowchart LR
A["① clearRect<br/>擦除"] --> B["② 绘制<br/>fillRect"]
B --> C["③ 更新<br/>x += speed"]
C --> D["④ requestAnimationFrame<br/>请求下一帧"]
D --> A
四步循环:擦 → 画 → 更新 → 请求下一帧。 任何 Canvas 游戏和动画都逃不出这个模式。
六、从 2D 到 3D:认识 Three.js
6.1 知识脉络
学完 Canvas 2D 再了解 3D,会发现底层逻辑一脉相承:
Canvas 2D API Three.js
────────────── ────────
getContext('2d') → 底层是 getContext('webgl')
fillRect / arc → BoxGeometry / SphereGeometry
fillStyle → Material(材质)
requestAnimationFrame → requestAnimationFrame(完全一样!)
Three.js 是把复杂的 WebGL 封装成了好用的 JavaScript 3D 库。原生的 WebGL 画一个三角形需要几十行代码,Three.js 几行就能搭出一个 3D 场景。
6.2 Three.js 三板斧:场景、相机、渲染器
对应 Canvas 2D 的 "画板 + 画笔 + 绘制",Three.js 有三个核心概念:
// Canvas 2D:canvas + getContext('2d') + fillRect
// Three.js:
const scene = new THREE.Scene(); // ① 场景(世界)
const camera = new THREE.PerspectiveCamera(75, w/h, 0.1, 1000); // ② 相机(眼睛)
const renderer = new THREE.WebGLRenderer(); // ③ 渲染器(画笔)
| Three.js 概念 | 类比 | 作用 |
|---|---|---|
| Scene(场景) | 世界 | 所有 3D 物体的容器 |
| Camera(相机) | 眼睛 | 决定"从哪个角度看",透视 vs 正交 |
| Renderer(渲染器) | 画笔 | 把场景拍下来,渲染到 <canvas> 上 |
6.3 画第一个 3D 方块
import * as THREE from 'three';
// 三板斧
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement); // 渲染器的 domElement 就是一个 <canvas>
// 创建一个红色方块 —— 对应 Canvas 2D 的 fillStyle + fillRect
const geometry = new THREE.BoxGeometry(1, 1, 1); // 几何体(形状)
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 }); // 材质(颜色/纹理)
const cube = new THREE.Mesh(geometry, material); // 组合 = 几何体 + 材质
scene.add(cube);
camera.position.z = 5; // 相机往后移,才能看到方块
// 动画循环 —— 和 Canvas 2D 一模一样!
function animate() {
renderer.render(scene, camera); // 渲染一帧(类似 ctx.fillRect)
cube.rotation.x += 0.01; // 绕 X 轴旋转
cube.rotation.y += 0.01; // 绕 Y 轴旋转
requestAnimationFrame(animate); // 帧循环 —— 完全一样!
}
animate();
6.4 Canvas 2D vs Three.js 对照表
| 概念 | Canvas 2D | Three.js |
|---|---|---|
| 上下文 | getContext('2d') | new THREE.WebGLRenderer() |
| 画形状 | fillRect(x, y, w, h) | new THREE.BoxGeometry(w, h, d) |
| 颜色 | fillStyle = 'red' | new THREE.MeshBasicMaterial({ color: 0xff0000 }) |
| 擦除 | clearRect() | renderer.render() 自动覆盖 |
| 动画 | requestAnimationFrame | requestAnimationFrame(完全相同) |
| 坐标系 | xy(二维) | xyz(三维)+ 相机视角 |
requestAnimationFrame是打通 2D 和 3D 的桥梁——帧循环的逻辑完全一样,区别只在于每一帧里画的是什么。
后续可以继续学习的方向:Canvas 交互(鼠标/键盘事件 → 做成游戏)、ECharts 数据可视化(底层也是 Canvas)、Three.js 进阶(灯光、纹理、物理引擎)。