这个东西
很重要
,是接下来所有一切操作的
开端
,因为我们画的是函数,一定要有个 x 轴的边界(当做参数传进来),那 y 轴呢,一般我们的网格都是正方形,所以这里让 y 轴的取值范围跟 x 轴一致即可。如果你不传 x 轴的两个边界值(lefxX、rightX),那我们会给个默认值[-canvas宽度的一半/100, canvas宽度的一半/100],因为一般画布的宽高都是几百,所以这里就简单的除以 100,你要除以 10 也是 ok 的。再次强调一下边界值这个东西很重要,因为接下来的操作都是基于边界值来绘制的。不信你可以尝试一下没有边界值的情况,那可能会无从下手🐶。
2、不是传入多少网格数就是多少网格
因为网格刻度是需要骚微取整的,比如 10、5、1、0.5、0.2、0.1 这样。什么意思呢,举个具体的例子🌰,比如 x 轴的取值范围是 (-5,5),网格的数量是10,那刚好就可以有 10 个网格,这样刻度就会挺整的(-5,-4,-3这样);如果网格数是9,那么我们也会有10个网格,刻度也是(-5,-4,-3这样),而不是(-5,-3.9,-2.8),不知道大家 get 到木有。所以网格大小是要稍微计算一下的,算是个小小的算法,下面是其中的一种解法👇🏻:
* 计算每一个网格的宽高大小,注意并不是 gridCount 是多少就会有多少网格
* 因为我们的坐标刻度通常都是 10、5、1、0.5、0.1 这个样子,大部分都是以 2、5、10 的倍数
* @param len x 或 y 轴代表的长度
* @param gridCount 网格个数
calcGridSize(len: number, gridCount: number): number[] {
let gridWidth = 1;
// 应该保留几位小数,避免浮点数
let fixedCount = 0;
// 事实上,要是图方便的话,你也可以直接用 unitX 来当做网格大小,不过记得要取整
let unitX = len / gridCount;
// 而这里呢,我们需要找到离 unitX 最近的(稍微偏整数的)值
// 首先要找到离 unitX 最近的 10 的整数倍,如 0.01、0.1、1、10、100
while (gridWidth < unitX) {
gridWidth *= 10
while (gridWidth / 10 > unitX) {
gridWidth /= 10
fixedCount++;
// 看看能不能再划分一次,如(/5 得到 0.02、0.2、20)、(/2 得到 0.05、5、50)
if (gridWidth / 5 > unitX) {
gridWidth /= 5;
fixedCount++;
} else if (gridWidth / 2 > unitX) {
gridWidth /= 2;
fixedCount++;
// 因为 x 轴长度和 y 轴的长度是一样的,所以可以这样赋值
return [gridWidth, gridWidth, fixedCount];
3、如何让坐标原点位于画布中心
这个比较抽象,所以我们看图。我们不要这样(注意看四周的网格):
我们要这样(注意看四周的网格,再和上图对比一下):
不知道大家 get 到木有,其实你可以从中间开始往上下、往左右画网格,不过这里我们还是从左到右画就行(做点简单的数学运算),直接看代码应该会清晰点,而且我还配了图😄:
drawGrid() {
const { width, height, leftX, rightX, leftY, rightY, xLen, yLen, gridCount, ctx2d } = this;
ctx2d?.save();
// 注意这里我们是将网格数作为配置项,而不是网格的宽高大小
const [gridWidth, gridHeight, fixedCount] = this.calcGridSize(xLen, gridCount);
// 由于计算会产生浮点数偏差,所以要控制下小数点后面的数字个数
this.fixedCount = fixedCount;
// 从左到右绘制竖线
for (let i = Math.floor(leftX / gridWidth); i * gridWidth < rightX; i++) {
// 绘制像素点 / 整个画布宽度 = 实际 x 值 / 实际表示的 x 轴长度
const x = (i * gridWidth - leftX) / xLen * width;
// i = 0 就说明是 y 轴,颜色加深
const color = i ? '#ddd' : '#000';
this.drawLine(x, 0, x, height, color)
this.fillText(String(this.formatNum(i * gridWidth, this.fixedCount)), x, height, this.fontSize, TextAlign.Center);
// 绘制横线也是和上面一样的方法,就是要注意画布的 y 轴向下,需要用 height 减一下,或者用 scale(1, -1);
for (let j = Math.floor(leftY / gridHeight); j * gridHeight < rightY; j++) {
let y = (j * gridWidth - leftY) / yLen * height;
y = height - y;
const color = j ? '#ddd' : '#000';
this.drawLine(0, y, width, y, color);
this.fillText(String(this.formatNum(j * gridHeight, this.fixedCount)), 0, y, this.fontSize, TextAlign.Middle);
ctx2d?.restore();
// 保留 fixedCount 位小数,整数不补零
formatNum(num: number, fixedCount: number): number {
return parseFloat(num.toFixed(fixedCount));
/** 绘制函数曲线,就是用一段段直线连起来 */
drawFn() {
const { width, height, leftX, leftY, xLen, yLen, steps, ctx2d } = this;
if (!ctx2d) return;
ctx2d.save();
this.fnList.forEach(fn => {
ctx2d.strokeStyle = (fn as any).color;
ctx2d.beginPath();
for (let i = 0; i < width; i += steps) {
// 小小的公式推导:像素点 / 画布宽 = x / 实际表示的 x 轴长度
const x = i / width * xLen + leftX;
let y = fn(x);
if (isNaN(y)) continue;
// 换算到具体绘制点
y = height - (y - leftY) / yLen * height;
// 在画布之外是不用绘制的所以用 moveTo 即可
if (i === 0 || y > height || y < 0) {
ctx2d.moveTo(i, y);
} else {
ctx2d.lineTo(i, y);
ctx2d.stroke();
ctx2d.restore();
这里想强调一点的就是在绘制前后时刻记得要 save 和 restore,并且这两个方法要配对使用。我们都应该听过 canvas 是基于状态管理的,如果你想要画一条红色的线,我们需要把画笔设置成红色,不使用 save 和 restore 的话,这个红色会影响到接下来的所有操作,所以一般我们要养成下面这样的习惯(尤其是平移、旋转和缩放等操作,不然一脸懵逼不是梦😳):