从 Canvas 2D 到 Three.js:我的 HTML5 绘图学习笔记

4 阅读5分钟

从零开始学 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 &#34;DOM 树&#34;
            DIV[&#34;<div>&#34;]
            CANVAS[&#34; ← 标签本身在这里&#34;]
            P[&#34;<p>&#34;]
        end
        subgraph &#34;Canvas 渲染区(像素)&#34;
            PX[&#34;fillRect 画的红方块 ← 只有像素,没有 DOM 节点&#34;]
        end
    end
    CANVAS -.->|&#34;getContext('2d')&#34;| 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 &#34;样式属性(画之前设置)&#34;
        FS[&#34;fillStyle<br/>填充色&#34;]
        SS[&#34;strokeStyle<br/>描边色&#34;]
        LW[&#34;lineWidth<br/>线宽&#34;]
    end
    subgraph &#34;绘制方法&#34;
        FR[&#34;fillRect()<br/>画实心矩形&#34;]
        SR[&#34;strokeRect()<br/>画空心矩形&#34;]
        CR[&#34;clearRect()<br/>擦除&#34;]
        FILL[&#34;fill()<br/>填充路径&#34;]
        STROKE[&#34;stroke()<br/>描边路径&#34;]
    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[&#34;① clearRect<br/>擦除&#34;] --> B[&#34;② 绘制<br/>fillRect&#34;]
    B --> C[&#34;③ 更新<br/>x += speed&#34;]
    C --> D[&#34;④ requestAnimationFrame<br/>请求下一帧&#34;]
    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 2DThree.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() 自动覆盖
动画requestAnimationFramerequestAnimationFrame完全相同
坐标系xy(二维)xyz(三维)+ 相机视角

requestAnimationFrame 是打通 2D 和 3D 的桥梁——帧循环的逻辑完全一样,区别只在于每一帧里画的是什么。


后续可以继续学习的方向:Canvas 交互(鼠标/键盘事件 → 做成游戏)、ECharts 数据可视化(底层也是 Canvas)、Three.js 进阶(灯光、纹理、物理引擎)。