canvas现在很好用的库有很多,但是体积都相对较大,在一些基本的网站上使用canvas库就得不偿失了,对项目的性能影响还是比较大的。于是学习canvas也是很重要的事情。这次已画折线图为例,从canvas基本api,canvas画图计算,canvas事件添加,canvas画布替代避免画布清除来讲解。说明都写在注释里面。
1 canvas基本api
HTML
<canvas id="canvas" width="800" height="600"></canvas>
JS
const canvas = document.getElementById("canvas"); // 获取画布
const ctx = canvas.getContext("2d"); // 获取2d画布环境
ctx.beginPath(); // 开始
ctx.moveTo(0, 0); // 画笔指定开始点
ctx.lineTo(10,10); // 连接当前端点和指定的坐标点
ctx.font = '16px SimHei'; // 字体
ctx.lineWidth = 1; // 设置线宽
ctx.strokeStyle = "red"; // 设置线的颜色
ctx.stroke(); // 沿着绘制路径的坐标点顺序绘制直线
.....
2 canvas画图计算
这次要画一个折线图需要计算的有:坐标轴,点(x,y)
首先计算坐标轴
// 假设这是我们获取的数据
const arr = [
{key: '1月', mount: 100},
{key: '2月', mount: 200},
{key: '3月', mount: 250},
{key: '4月', mount: 110},
{key: '5月', mount: 500},
{key: '6月', mount: 600},
];
// 获取画布的高宽
const cw = canvas.width;
const ch = canvas.height;
// 内边居
const padding = 80;
//原点,x终点,y终点
const origin = {x: padding, y: ch - padding};
const xAisa = {x: cw - padding, y: ch - padding};
const yAisa = {x: padding, y: padding};
// 画X轴
ctx.beginPath(); // 开始
ctx.moveTo(origin.x, origin.y); // 移动到坐标原点
ctx.lineTo(xAisa.x, xAisa.y); // 画x轴
ctx.lineTo(xAisa.x - 10, xAisa.y - 10); // 画坐标轴上三角
ctx.moveTo(xAisa.x, xAisa.y);
ctx.lineTo(xAisa.x - 10, xAisa.y + 10); // 画坐标轴下三角
ctx.font = '16px SimHei'; // 设置字体
ctx.lineWidth = 1; // 设置线宽
ctx.strokeStyle = "red"; // 设置线的颜色
const avW = (cw - 2 * padding - 20) / (arr.length - 1); // X轴平均长度
for (let i = 0; i < arr.length; i++) {
// 画分度
ctx.moveTo(origin.x + i * avW, origin.y);
ctx.lineTo(origin.x + i * avW, origin.y - 10);
// 填充文字
const textWidth = ctx.measureText(arr[i].key).width;
ctx.fillText(
arr[i].key,
origin.x + i * avW - textWidth / 2,
origin.y + 32);
}
ctx.stroke();
// 画Y轴 类似
ctx.beginPath();
ctx.moveTo(origin.x, origin.y);
ctx.lineTo(yAisa.x, yAisa.y);
ctx.lineTo(yAisa.x - 10, yAisa.y + 10);
ctx.moveTo(yAisa.x, yAisa.y);
ctx.lineTo(yAisa.x + 10, yAisa.y + 10);
const avgy = (ch - 2 * padding - 20) / (arr.length);
const maxValueY = Math.max(...arr.map(x => x.mount)); //最大值
const average = Math.floor(maxValueY / arr.length);
for (let i = 0; i < arr.length + 1; i++) {
ctx.moveTo(origin.x, origin.y - i * avgy);
ctx.lineTo(origin.x + 10, origin.y - i * avgy);
const textWeight = ctx.measureText(average * i).width;
ctx.fillText(average * i, origin.x - textWeight - 5, origin.y - i * avgy + 6);
}
ctx.font = '16px SimHei';
ctx.lineWidth = 1;
ctx.strokeStyle = 'black';
ctx.stroke();
// 画折线
ctx.beginPath();
for (let i = 0; i < arr.length; i++) {
const y = origin.y - Math.floor(arr[i].mount / maxValueY * (ch - 2 * padding - 20));
if (i === 0) {
ctx.moveTo(origin.x + i * avW, y)
} else {
ctx.lineTo(origin.x + i * avW, y)
}
// ctx.fillText(arr[i].mount, origin.x + i * avW, y)
}
ctx.strokeStyle = 'yellow';
ctx.stroke();
// 绘制小圆点
ctx.beginPath();
for (let i = 0; i < arr.length; i++) {
const y = origin.y - Math.floor(arr[i].mount / maxValueY * (ch - 2 * padding - 20));
// 外圆
ctx.beginPath();
ctx.arc(origin.x + i * avW, y, 8, 0, Math.PI * 2);
ctx.fillStyle = '#6a83ff';//填充颜色
ctx.strokeStyle = '#4d4e53';//边框颜色
ctx.fill();
// 内圆
ctx.beginPath();
ctx.arc(origin.x + i * avW, y, 4, 0, Math.PI * 2);
ctx.fillStyle = '#ffffff';//填充颜色
ctx.strokeStyle = '#ffffff';//边框颜色
ctx.fill();
ctx.closePath();
}
3 canvas事件
canvas内部元素,我们不能直接添加事件,需要我们拿到事件对象的坐标点和canvas内部区域来比较,获取方法有
// 获取鼠标位置
1 function getEventPosition(ev) {
let x;
let y;
if (ev.layerX || ev.layerX === 0) {
x = ev.layerX;
y = ev.layerY;
} else if (ev.offsetX || ev.offsetX === 0) { // Opera
x = ev.offsetX;
y = ev.offsetY;
}
return {x: x, y: y};
}
2 getBoundingClientRect()这个需要我们再去扩展计算
4 canvas DOM替代发
因为我们在动态画图时;有时需要我们再清除,这个时候我们用canvas画的图就很不容易处理,所以需要我们用生成dom的方式来替换canvas画布;
正如 那些tips都是dom并非canvas画的。
// tip添加
const convasbox = document.getElementById('convasbox');
const tipsElement = document.createElement('div');
tipsElement.innerHTML = `<div> <span class="value"></span></div>`;
tipsElement.style.visibility = 'hidden';
convasbox.appendChild(tipsElement);
// 添加事件
canvas.addEventListener('mousemove', function (ev) {
const position = getEventPosition(ev);
const isXMultiple = (position.x - padding) % avW; // 判断坐标位置
const Quotient = (position.x - padding) / avW;
if (isXMultiple === 0) {
tipsElement.style.visibility = 'visible';
tipsElement.querySelector('.value').innerText = arr[Quotient].mount;
console.log(`translate(${position.x}+px,${position.y}+px)`)
tipsElement.style.transform = `translate(${position.x}px,${position.y}px)`
}
});