原来 ECharts 和飞机大战是同一个东西 —— 我终于搞懂了 Canvas 的本质

我一直觉得 ECharts 和网页游戏是两个完全不搭边的技术领域,一个是做数据报表的,一个是写游戏玩的,直到这周我同时开了两个项目,一个用 ECharts 画销售柱状图,一个用 Canvas 写飞机大战,才突然拍大腿发现:它们居然是同一个妈生的 —— 都是在一块画布上一笔一笔画出来的。

帅哥美女们帮我的掘金点点赞呗😘👍 完整文章地址👉:https://juejin.cn/post/7651524453219647534

说出来有点丢人,从来没打开过控制台看过 Echarts 到底是怎么渲染的。直到这次写完柱状图,随手点了一下审查元素,看到一个大大的 canvas 标签占满了整个图表区域,当时我就懵了。原来那些带渐变、带阴影、带 hover 效果的柱子,不是用 div 拼出来的,也不是 svg,居然是用 js 一行一行画在画布上的。

先从那个龙哥集团的柱状图说起

这周帮朋友做了个简单的销售数据看板,需求很简单,就是把龙哥集团 2025 年奶龙玩具的月度销售额做成一个柱状图。我想都没想就直接用 Vite 初始化了一个原生 JS 项目,安装了 ECharts。

项目结构特别简单,就是 Vite 的标准结构。我一开始把所有代码都写在 main.js 里,包括数据和图表配置,后来觉得太乱了,就把数据单独抽成了一个 data.js 文件。这个好习惯真的要养成,后来改数据的时候不用在一堆配置里翻来翻去。

▼
javascript
复制代码
// data.js /** * 龙哥集团 - 奶龙玩具 2025 年月度销售数据 * 单位:百万元 */ export const company = '龙哥集团' export const months = [ '1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月' ] // 奶龙玩具月销售额(百万元),模拟一年的涨跌趋势 export const salesData = [8.5, 7.2, 9.8, 11.3, 10.6, 13.5, 14.2, 15.8, 13.1, 12.4, 16.7, 18.2]

然后在 main.js 里引入这些数据,初始化 ECharts。说真的,ECharts 的配置项虽然多,但是写多了就会发现套路都差不多:先拿一个 DOM 元素当容器,然后 init 一个实例,再传一个 option 进去就完事了。

▼
javascript
复制代码
// main.js import './style.css' import * as echarts from 'echarts' import { company, months, salesData } from './data.js' document.querySelector('#app').innerHTML = ` <div class="chart-header"> <h1>${company}</h1> <p class="chart-subtitle">奶龙玩具 · 2025 年月度销售额</p> </div> <div id="chart"></div> ` // 初始化 ECharts 柱状图 const chartDom = document.getElementById('chart') const chart = echarts.init(chartDom) const option = { tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, valueFormatter: (value) => value + ' 百万元' }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, xAxis: { type: 'category', data: months, axisLabel: { fontSize: 13 } }, yAxis: { type: 'value', name: '销售额(百万元)', min: 0, axisLabel: { formatter: '{value}' } }, series: [ { name: '销售额', type: 'bar', data: salesData, itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: '#c084fc' }, { offset: 1, color: '#7c3aed' } ]), borderRadius: [4, 4, 0, 0] }, emphasis: { itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: '#a855f7' }, { offset: 1, color: '#5b21b6' } ]) } } } ] } chart.setOption(option) // 响应式缩放 window.addEventListener('resize', () => chart.resize())

这里有个小坑,我一开始写渐变的时候,把 LinearGradient 的坐标搞反了,写成了 (0, 1, 0, 0),结果渐变变成了从上到下浅到深,特别丑。查了半天文档才发现,前两个参数是渐变起点的 x 和 y 坐标,后两个是终点的坐标。(0,0) 是左上角,(0,1) 是左下角,所以从上到下的渐变应该是 (0,0,0,1)。

最终效果就是这样的,看起来还挺专业的对吧?

image.png

好奇心害死猫,我想知道 ECharts 到底在干嘛

看着这个 canvas 标签,我突然很好奇,如果不用 ECharts,我自己能不能用纯 Canvas 画出一个类似的柱状图?于是我开了第二个项目,准备从零开始学 Canvas。

我先写了一个最简单的 demo,画两个矩形,一个填充的,一个描边的。

▼
html
复制代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Canvas 入门</title> </head> <body> <canvas id="myCanvas" width="600" height="400" style="border: 1px solid #333;"> 你的浏览器不支持Canvas </canvas> <script> // 拿到画布元素 const canvas = document.querySelector('#myCanvas'); // 拿到绘画的上下文对象,这就是我们的画笔 const ctx = canvas.getContext('2d'); // 设置填充颜色,画一个蓝色的实心矩形 ctx.fillStyle = '#4299e1'; ctx.fillRect(20, 20, 100, 100); // 设置边框颜色和宽度,画一个红色的空心矩形 ctx.strokeStyle = '#f56565'; ctx.lineWidth = 5; ctx.strokeRect(150, 20, 100, 100); // 清除一个矩形区域,这里我故意清掉了一点红色边框 ctx.clearRect(140, 10, 100, 100); </script> </body> </html>

跑起来一看,我突然明白了什么。原来 Canvas 就是一张白纸,getContext ('2d') 就是给你一支画笔。你告诉画笔什么颜色,它就画什么颜色;你告诉它画个矩形,它就画个矩形;你告诉它擦掉哪里,它就擦掉哪里。就这么简单。

注意这个 clearRect,我当时把它写在了 strokeRect 后面,结果红色边框的左上角被清掉了一块。这个细节后来帮了我大忙,写动画的时候全靠它。

动画的本质,就是擦了再画

会画静态的矩形了,那怎么让它动起来?我第一反应就是用 setInterval,每隔 16 毫秒(大概 60 帧)移动一下矩形的位置。结果跑起来卡得要死,还有严重的残影。

后来查了才知道,setInterval 根本就不是用来做动画的。它的时间不准,而且和屏幕的刷新率不同步,很容易出现掉帧和卡顿。浏览器专门给我们提供了一个做动画的 API,叫 requestAnimationFrame。

▼
html
复制代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Canvas 动画</title> </head> <body> <canvas id="myCanvas" width="600" height="400" style="border: 1px solid #333;"> 你的浏览器不支持Canvas </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 + width > canvas.width) { x = 0; } // 递归调用自己,请求下一帧 requestAnimationFrame(animate); } // 启动动画 animate(); </script> </body> </html>

跑起来的那一刻,我真的被惊艳到了,丝滑得一批。原来动画这么简单,就是三步:

  1. 把上一帧的东西全部擦掉
  2. 在新的位置画上新的东西
  3. 重复上面两步

只要速度够快,每秒重复 60 次,人眼就会觉得这是一个连续的动画。这个原理太重要了,不管是简单的矩形移动,还是复杂的 3D 游戏,本质上都是这个逻辑。

image.png

那飞机大战呢?不就是多画几个东西吗

既然一个矩形能动,那我画个飞机,再画几个敌机,再画几颗子弹,不就是飞机大战了吗?说干就干,我又开了一个项目。

项目结构和 ECharts 那个几乎一模一样,都是 Vite 初始化的,src 里就一个 main.js。

其实整个游戏的核心逻辑还是那个 animate 函数,只不过现在每一帧要做的事情变多了:

  • 擦干净画布
  • 画玩家飞机
  • 画所有子弹
  • 画所有敌机
  • 更新飞机的位置(根据键盘输入)
  • 更新所有子弹的位置(向上飞)
  • 更新所有敌机的位置(向下飞)
  • 检测子弹和敌机的碰撞
  • 检测敌机和玩家的碰撞
  • 计算分数
  • 请求下一帧

我当时写的时候,先让飞机能上下左右移动,然后加了边界限制,不让它飞出屏幕。然后加了空格键发射子弹,再然后加了随机生成敌机。最后加了碰撞检测和游戏结束逻辑。一步一步来,其实没那么难。

最终做出来的效果是这样的:

image.png

虽然画面很简陋,但是该有的功能都有了。而且整个游戏没有用任何第三方引擎,纯原生 Canvas 写的,加起来不到 300 行代码。

绕了一圈,回到 ECharts

现在再回头看 ECharts,我突然觉得它一点都不神秘了。它其实就是把我们手动画矩形、画文字、画动画的过程,全部封装成了一个个配置项。你告诉它 x 轴是什么数据,y 轴是什么数据,柱子是什么颜色,它就帮你在 Canvas 上一笔一笔画出整个图表。

你看 ECharts 的 series 里的 type 是 'bar',它就循环遍历你的数据,给每个数据点算好位置和宽度,然后调用 fillRect 画一个个矩形。你加了 tooltip,它就在你鼠标移动的时候,计算你鼠标在哪个柱子上,然后画一个浮层出来。你加了渐变,它就帮你创建一个 LinearGradient 对象,然后设置给 fillStyle。

原来我之前用了那么久的 ECharts,其实一直在用别人写好的 Canvas 绘图库。

这周踩过的几个坑,你们别再踩了

第一个坑,Canvas 的宽高一定要写在标签属性里,不能只写 CSS。如果你只在 CSS 里设置 width 和 height,Canvas 会把它默认的 300x150 的画布拉伸到你设置的大小,画面会变得特别模糊。正确的做法是:

▼
html
复制代码
<!-- 正确 --> <canvas id="myCanvas" width="600" height="400"></canvas> <!-- 错误 --> <canvas id="myCanvas" style="width: 600px; height: 400px;"></canvas>

第二个坑,clearRect 一定要写在每一帧的最开头。我一开始把它写在了 animate 函数的最后,结果整个屏幕都被画满了,全是残影。因为你先画了新的东西,再擦掉上一帧的,等于什么都没擦掉。

第三个坑,requestAnimationFrame 是递归调用的,一定要在 animate 函数里再调用一次自己。我一开始忘了写,结果动画只跑了一帧就停了,调试了半天才发现。

最后说几句

这周最大的收获,不是学会了怎么用 Canvas 写飞机大战,也不是更熟练地使用 ECharts,而是明白了一个道理:很多看起来复杂的技术,底层都是由非常简单的原理组成的。

ECharts 再强大,底层也是 Canvas 的 fillRect、strokeRect 这些基础 API。飞机大战再好玩,本质上也是 “擦了再画” 这个简单的循环。

当然,Canvas 也不是万能的。如果只是做普通的网页布局,用 DOM 肯定更方便,也更容易维护。但是如果你需要做高性能的动画、游戏、或者高度自定义的数据可视化,Canvas 绝对是你的不二之选。

如果你也踩过 Canvas 的这些坑,或者有更好的理解,欢迎在评论区留言,我也想看看大家都是怎么玩的。

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP