欢迎光临
我们一直在努力

HTML5 Canvas 从入门到动画:把浏览器的画布玩明白

一、背景:为什么是 Canvas

734a739a7ea981322debd55aa58fd4fe.jpg HTML5 带来的新特性里,Canvas 可能是最「不像 Web」的那一个。

我们习惯了用 div、span、section 拼页面,习惯了 DOM 树、CSS 盒模型、BFC 这些概念。但 Canvas 完全不按这套逻辑出牌——它只有一个标签 <canvas>,剩下的全靠 JS 一笔一笔画出来。没有 DOM 节点,没有样式表,你拿到的是一块像素级的画布,想画什么,代码说了算。

这听起来很原始,但恰恰是这种「原始」给了它极大的发挥空间。数据可视化(ECharts 底层就是 Canvas/WebGL)、网页游戏(从 4399 Flash 时代到今天的 HTML5 游戏)、酷炫的交互页面——Canvas 几乎是这些场景的唯一解。

顺便一提:如果追求 3D 效果,可以用 getContext('3d') 走 WebGL 路线,配合 Three.js 等引擎释放 GPU 显存能力。近两年 AI 游戏爆发、物理大模型等话题也常和 WebGL/WebGPU 挂钩。但本文的焦点是 2D——先把手绘基本功练好。

本文用两个最小化的 HTML 文件,把 Canvas 的核心概念拆清楚。不依赖任何框架,每个例子都是十几行代码,打开浏览器就能跑。


二、目标:这个阶段要解决什么问题

一句话概括:理解 Canvas 的绘制模型和动画模型,能独立写出静态图形和帧动画。

阶段内容对应文件
第一阶段 Canvas 标签、坐标系统、基本图形绘制(矩形)、填充与描边、局部擦除 1.html
第二阶段 帧动画循环、requestAnimationFrame 原理、与 setInterval 的对比 2.html

这两个阶段加起来不超过 50 行 JS,但它们是所有 Canvas 应用的基石。无论之后做游戏还是做可视化图表,核心套路都是这两步。


三、第一阶段:静态绘制 —— 把形状画上去

3.1 Canvas 标签与坐标系统

a1188f1b79c167dff48740e8d7393ed5.jpg

<canvas id="canvas" width="800" height="600"
style="border: 1px solid #333;">
你的浏览器不支持 Canvas 画布
</canvas>

几个关键点:

  • width 和 height 是 HTML 属性,不是 CSS。它们决定画布的实际像素分辨率(这里是 800×600)。如果只用 CSS 设宽高,画布内部仍是默认的 300×150,然后被拉伸——这是高频踩坑点,后文会展开。
  • 标签内的文字是降级内容。只有不兼容 Canvas 的古董浏览器(IE8 等)会渲染这段文字,现代浏览器会忽略它。
  • 边框只是辅助线。style="border: 1px solid #333" 让我们能看到画布的边界,实际项目中通常不加。

Canvas 的坐标系统不同于数学课本——原点 (0, 0) 在左上角,x 轴向右为正,y 轴向下为正:

(0,0) ──────────── x →

│ ┌──────────┐
│ │ 矩形 │
│ └──────────┘
y

这个坐标系一旦记错,后面所有定位都会跑偏。

3.2 getContext:拿到画笔

const canvas = document.querySelector('#canvas')
const ctx = canvas.getContext('2d')

getContext('2d') 返回的是一个 2D 渲染上下文对象——你可以把它理解为一支画笔,上面挂满了绘制方法。之后所有的 fillRect、strokeRect、clearRect 都通过 ctx 调用。

这个命名其实很有深意:它叫「上下文」而不是「画笔」,因为除了画笔的功能,它还管理着全局的绘图状态——当前的填充色、描边色、线宽、透明度、变换矩阵等等。你每次设置状态,影响的是后续所有绘制,而不是某一次调用。 这一点后面会反复体现。

3.3 填充矩形:fillRect + fillStyle

ctx.fillStyle = 'blue' // 设置填充颜色
ctx.fillRect(20, 20, 100, 80)

fillRect(x, y, width, height) 的四个参数含义:

fillRect(20, 20, 100, 80)
↑ ↑ ↑ ↑
x y 宽 高
(距离左上角的偏移)

fillStyle 的值可以是 CSS 颜色字符串——'blue'、'#4299e1'、'rgba(255,0,0,0.5)' 都可以。它是一个全局状态属性:一旦设置,之后所有的 fillRect() 都会用这个颜色,直到你再次修改它。

3.4 描边矩形:strokeRect + strokeStyle + lineWidth

ctx.strokeStyle = '#f56565' // 描边颜色
ctx.lineWidth = 5 // 线宽,单位 px
ctx.strokeRect(150, 20, 100, 80)

strokeRect 和 fillRect 是姊妹方法,但画的是空心矩形。注意 strokeStyle 和 fillStyle 是两套独立的状态——你可以同时设置两者,后续绘制时各取所需。

lineWidth 控制描边粗细。这里设为 5px 是为了让效果明显,实际项目中按需求来。同样,lineWidth 是全局状态,设一次,所有后续描边都会用这个线宽。

3.5 局部擦除:clearRect

ctx.clearRect(50, 50, 40, 30)

clearRect 把指定矩形区域内的像素「擦掉」,恢复为透明。它和 fillRect 的参数格式完全一致。

在 1.html 中,这个擦除操作是为了演示:你可以在蓝色矩形上「挖」一个洞。它的实际意义远比演示重要——在帧动画中,每一帧的第一步就是 clearRect 清空整张画布,再画上新的内容,这是整个动画机制的起点。

3.6 1.html 完整回顾

<canvas id="canvas" width="800" height="600"
style="border: 1px solid #333;">
你的浏览器不支持 Canvas 画布
</canvas>
<script>
const canvas = document.querySelector('#canvas')
const ctx = canvas.getContext('2d')

ctx.fillStyle = 'blue'
ctx.fillRect(20, 20, 100, 80)

ctx.strokeStyle = '#f56565'
ctx.lineWidth = 5
ctx.strokeRect(150, 20, 100, 80)

ctx.clearRect(50, 50, 40, 30)
</script>

运行效果:蓝色填充矩形 + 红色描边矩形 + 蓝色矩形上被擦掉的一块。

代码量不过十来行,但涉及了 Canvas 最核心的四个概念:坐标系统、上下文对象、填充/描边双轨制、全局状态。 这些概念会贯穿所有 Canvas 应用。


四、第二阶段:帧动画 —— 让画面动起来

image.png 静态绘图只是第一步。Canvas 真正的生命力在于动画——连续、流畅、可控的动画。

4.1 动画的本质:擦掉 → 重画 → 循环

回忆一下小时候画翻页动画(flip book):在每一页纸的边缘画一个略有位移的小人,快速翻动时小人就「动」了。

Canvas 的动画逻辑完全一致:

┌──────────┐ ┌──────────┐ ┌──────────┐
│ 帧 1 │ ──→ │ 帧 2 │ ──→ │ 帧 3
│ │ │ │ │ │
│ ▓▓▓▓ │ │ │ │ │
│ │ │ ▓▓▓▓ │ │ │
│ │ │ │ │ ▓▓▓▓ │
└──────────┘ └──────────┘ └──────────┘
擦除 擦除 擦除
在新位置 在新位置 在新位置
画矩形 画矩形 画矩形

写成分步操作就是:

  • clearRect() —— 擦掉上一帧的全部内容
  • 更新图形的位置(x += speed)
  • 在新的位置画出图形(fillRect)
  • 重复步骤 1,形成循环
  • 4.2 为什么不能用 setInterval

    直觉告诉我们:要做循环,用一个定时器就好了嘛——比如 60fps,就 setInterval(fn, 16)。

    这是一个非常自然的想法,但恰恰是常见的误区。

    问题主要有两个:

    第一,帧率不对齐。 setInterval 的触发时机和显示器的刷新周期是完全独立的两个时钟。如果显示器是 60Hz(约 16.67ms 一次刷新),而 setInterval 按 16ms 触发,两个节奏就会逐渐错位,导致:

    • 丢帧:一次刷新周期内没有新的画面准备好,显示器重复显示上一帧
    • 撕裂:一帧画面在绘制中途遇到显示器刷新,上下半屏来自不同帧

    setInterval(fn, 16)
    |──16ms──|──16ms──|──16ms──|──16ms──|
    屏幕刷新: ↓ ↓ ↓ ↓ ← 节奏脱节

    第二,后台不暂停。 当你切到其他标签页,setInterval 照样在后台跑——无意义的 CPU 消耗,对移动端设备尤其不友好。

    4.3 requestAnimationFrame:和屏幕同步的动画调度器

    requestAnimationFrame(简称 rAF)是浏览器专门为动画设计的 API。它的行为:

    • 在浏览器下一次重绘前执行你的回调,回调频率自动匹配显示器的刷新率
    • 切到后台自动暂停,切回来自动恢复
    • 页面不可见时完全不会触发,省电省资源

    function animate() {
    // 1. 擦掉上一帧
    ctx.clearRect(0, 0, canvas.width, canvas.height)

    // 2. 画新的一帧
    ctx.fillStyle = '#4299e1'
    ctx.fillRect(x, y, width, height)

    // 3. 更新位置(为下一帧准备)
    x += speed
    if (x > canvas.width) {
    x = -width // 超出画布后从左边重新进入
    }

    // 4. 注册下一帧 —— 递归调用
    requestAnimationFrame(animate)
    }

    animate() // 启动循环

    对比两个方案:

    requestAnimationFrame 的工作方式:
    屏幕刷新: ↓ ↓ ↓
    回调触发: ↓ ↓ ↓
    绘制帧1 绘制帧2 绘制帧3
    ← 完全同步 →

    setInterval(fn, 16) 的工作方式:
    定时器: |──16ms──|──16ms──|──16ms──|
    屏幕刷新: ↓ ↓ ↓ ↓
    可能不同步,丢帧或撕裂风险

    关键理解:rAF 不是「更精确的定时器」,它是「和浏览器渲染管线挂钩的调度器」。 它把控制权交给了浏览器——让浏览器在最合适的时机执行你的绘制逻辑,而不是你凭感觉估计一个毫秒数去叫浏览器刷新。

    4.4 2.html 完整回顾

    <canvas id="myCanvas" width="600" height="400"
    style="border:1px solid #333;">
    你的浏览器不支持Canvas(旧IE会显示这段文字)
    </canvas>
    <script>
    const canvas = document.querySelector('#myCanvas')
    const ctx = canvas.getContext('2d')

    let x = 20
    const y = 20
    const width = 100
    const height = 80
    const speed = 3

    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()
    </script>

    运行效果:一个蓝色矩形在画布中从左到右平滑移动,超出右边界后从左侧重新出现。

    20 多行代码,包含了动画的全部要素:状态变量、帧更新逻辑、边界处理、rAF 递归循环。这个循环一旦建立起来,剩下的就是往循环里加东西——加更多图形就是更复杂的动画,加交互就是游戏,加数据驱动就是实时可视化。


    五、设计:Canvas 应用的分层模型

    从 1.html 和 2.html 中,可以抽象出一个通用的 Canvas 应用架构。虽然这两个 demo 小到不需要显式分层,但分层意识是控制复杂度的关键:

    ┌────────────────────────────────────┐
    │ 初始化层 │
    │ – 获取 canvas 元素 │
    │ – getContext('2d')
    │ – 设置画布尺寸 │
    │ – 初始化状态变量 │
    └────────────────────────────────────┘


    ┌────────────────────────────────────┐
    │ 帧循环层 │
    │ │
    │ ┌──────────┐ ┌───────────┐ │
    │ │ update() │──→│ render() │ │
    │ │ (数据更新)│ │ (画面绘制) │ │
    │ └──────────┘ └───────────┘ │
    │ │
    │ requestAnimationFrame(loop)
    └────────────────────────────────────┘


    ┌────────────────────────────────────┐
    │ 交互层 │
    │ – 键盘事件 │
    │ – 鼠标/触摸事件 │
    │ – 将用户输入转为状态变更 │
    └────────────────────────────────────┘

    • update 只改数据:改变 x、y、score 等状态变量,不碰 ctx
    • render 只画画面:读取状态变量,调用 Canvas API 绘制,不改数据

    这个职责分离在小项目里看不出威力,但当项目膨胀到几百行时,它几乎是保持清醒的唯一手段。


    六、实际应用举例

    掌握了基础的绘制和动画能力后,Canvas 的应用场景非常多:

    6.1 数据可视化

    ECharts 的底层渲染引擎就使用了 Canvas(2D 模式)和 WebGL(3D 模式)。你画的每一个柱状图、折线图、饼图,本质上就是 Canvas 的 fillRect、lineTo、arc 的一层高级封装。理解了 Canvas 的基本功,看 ECharts 的源码就不再是黑盒。

    6.2 网页游戏

    我在学习完上述基础后,动手做了一个「打飞机」小游戏,完整覆盖了:

    • 玩家飞机(Path 绘制 + 渐变色引擎火焰)
    • 敌人(两种类型,不同生命值和分值)
    • 子弹(线性渐变,从白到黄)
    • 粒子爆炸特效(每帧衰减透明度)
    • 星空滚动背景(80 颗星,不同速度的视差效果)
    • 键盘 + 触摸双模输入
    • 得分系统 + 难度递增 + Game Over / 重新开始

    全部代码约 400 行,一个 main.js 文件搞定。核心技术就是本文讲的 clearRect + 绘制 + rAF 循环,再加一套碰撞检测(矩形中心距离判定)。

    6.3 酷炫页面效果

    Canvas 可以做粒子背景、鼠标跟随特效、页面转场动画等。很多所谓的「炫酷官网」就是在一块全屏 Canvas 上画出来的,比如粒子连线的网络背景、鼠标位置的引力场等等。


    七、踩坑:几个容易踩的细节

    7.1 Canvas 的 CSS 宽高 ≠ 属性宽高

    这是 Canvas 新手最常见的坑,值得单独列出来。

    Canvas 的默认内部分辨率是 300×150。如果你只用 CSS 的 width: 100%; height: 100% 去拉伸它,画布的实际绘制区域仍然是 300×150,只是被 CSS 缩放放大了——结果就是画面模糊,并且坐标计算全部不对。

    // 正确做法:设置属性
    canvas.width = window.innerWidth
    canvas.height = window.innerHeight

    // CSS 只负责填满容器
    // canvas { display: block; width: 100%; height: 100%; }

    记忆法则:属性决定画布精度,CSS 决定画布在页面中占多大地方。 两者各司其职。

    7.2 fillStyle 是全局状态,不是参数

    Canvas 的 API 设计中,颜色不是作为参数传给 fillRect,而是先设置到 ctx.fillStyle 上,再调用绘制方法。这跟 SVG 或 CSS 的思维方式完全不同。

    它的好处是减少参数传递(对于连续画多个同色图形很方便),代价是如果不注意状态管理,就会出现「上一段的颜色泄漏到下一段」的问题:

    ctx.fillStyle = 'red'
    ctx.fillRect(10, 10, 50, 50)

    ctx.fillStyle = 'blue'
    ctx.fillRect(70, 10, 50, 50)

    // 此时 fillStyle 是 'blue',后续不设置的话全是蓝色

    如果状态混乱了,用 ctx.save() 和 ctx.restore() 做快照/还原,它们保存和恢复的是整个绘图状态栈。

    7.3 clearRect 不调用会导致残影

    在 2.html 中如果注释掉 ctx.clearRect(0, 0, canvas.width, canvas.height),你会发现矩形移动时留下一条长长的「尾巴」。这是因为 Canvas 不会自动清空——每一帧的新绘制叠加在上一帧的画面上。

    这条尾巴既是 bug 也是艺术——有些特效(拖尾、画笔、流光)恰恰是利用了不清空画布的效果。但大多数时候,请记得在每帧开头 clearRect。

    7.4 动画不流畅?检查这些

    如果动画出现卡顿,按以下顺序排查:

  • 是不是用了 setInterval 而不是 rAF——最常见的原因
  • 单帧计算量过大——一帧内做了太多操作(几百个粒子、复杂的路径计算),超过 16.67ms 的预算
  • 在帧循环里创建对象——频繁的 GC(垃圾回收)会导致间歇性卡顿,考虑用对象池复用
  • 浏览器节流——检查是否在后台标签页运行(rAF 会自动暂停,这是正常的)

  • 八、复盘:从 demo 到产品的距离

    8.1 两个 demo 教会了我们什么


    结语

    HTML5 Canvas 诞生超过十年了。它的 API 不算大,但设计得很干净——坐标系统、上下文对象、状态机模型,一旦理解就能灵活运用。

    如果你对 Canvas 的印象还停留在「那个画图的标签」,不妨花十分钟把 1.html 和 2.html 的代码自己敲一遍。看到那个小蓝块在屏幕上稳定地跑起来的那一刻,你对「浏览器能做什么」的理解会完全不同。

    赞(0)
    未经允许不得转载:171主机测评 » HTML5 Canvas 从入门到动画:把浏览器的画布玩明白
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址