记录一些canvas的练习代码
基础概念:Math.PI
是半圆,Math.PI * 2
是整圆,Math.PI/2
是1/4圆,弧度与角度公式如下
// 一弧度
rad = (Math.PI * deg) / 180
// 一角度
deg = (rad * 180) / Math.PI
1. 绘制圆环进度
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8" /></head>
<style>
body{background-color: #F7F7FA;}
#canvas{border: 1px solid rgba(0, 0, 0, .1);}
</style>
<body><canvas width="400" height="400" id="canvas"></canvas></body>
</html>
const canvas = document.querySelector("#canvas");
const ctx = canvas.getContext("2d");
// 获取画布宽、高
const { width, height } = canvas;
// 画布中心点
const centerX = width / 2;
const centerY = height / 2;
// 画圆的半径
const radius = 100;
// 总进度 按100计算
const total = 100;
// 圆环宽度
const lineWidth = 4;
const lineColor = '#FF4750';
// 底圆宽度
const bottomLineWidth = 4;
const bottomLineColor = '#FFFFFF';
// 圆环进度
const process = 80;
// 获取dpr
const dpr = window.devicePixelRatio;
// 开始绘制
draw();
function draw() {
hd(canvas, ctx);
// 动画方式
drawAnimate()
// 无动画方式
// drawBottomCircle()
// drawCircle(process)
// drawText(process);
}
// 动画方式绘制
function drawAnimate() {
let curProcess = 0;
function animateProcess() {
curProcess = curProcess + 1;
const easeProgress = easeOutCubic(curProcess / process);
if (curProcess < process) {
// TODO:每次重绘清除画布,不清除会一直在原有的画布上绘制,产生毛刺或其它影响
ctx.clearRect(0, 0, width, height);
drawBottomCircle();
drawCircle(easeProgress * process);
drawText(easeProgress * process);
raf(animateProcess);
}
}
raf(animateProcess);
}
// 绘制底圆
function drawBottomCircle() {
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
ctx.lineWidth = bottomLineWidth * dpr;
ctx.strokeStyle = bottomLineColor;
ctx.lineCap = "round";
ctx.stroke();
}
// 绘制进度圆环
function drawCircle(process) {
// console.log('process',process)
const progress = process / total;
ctx.beginPath();
ctx.arc(
centerX,
centerY,
radius,
-Math.PI / 2,
Math.PI * 2 * progress - Math.PI / 2
);
ctx.lineWidth = lineWidth * dpr;
//创建渐变对象
const gradient = ctx.createLinearGradient(0, 0, width, 0);
//颜色断点
gradient.addColorStop(0, "#FFCDA0");
gradient.addColorStop(1, "#FF4750");
// 填充颜色
ctx.strokeStyle = gradient;
// ctx.strokeStyle = lineColor;
ctx.lineCap = "round";
ctx.stroke();
ctx.closePath();
}
// 进度文本
function drawText(process) {
const progress = process / total;
ctx.beginPath();
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "#FF4750";
ctx.font = `bold 36px sans-serif`;
ctx.fillText(Math.ceil(progress * 100), centerX, centerY);
ctx.closePath();
}
// 动画计时器
function raf(fn) {
if (window.requestAnimationFrame) {
return window.requestAnimationFrame(fn);
} else {
return setTimeout(fn, 1000 / 60);
}
}
// 画布高清
function hd(canvas, ctx) {
canvas.width = Math.round(width * dpr);
canvas.height = Math.round(height * dpr);
canvas.style.width = width + "px";
canvas.style.height = height + "px";
ctx.scale(dpr, dpr);
return canvas;
}
// 缓动动画函数
function easeOutCubic(x) {
return 1 - Math.pow(1 - x, 3);
}
效果如下图:
2. 绘制雷达图
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8" /></head>
<style>
body{background-color: #F7F7FA;}
#canvas{border: 1px solid rgba(0, 0, 0, .1);}
</style>
<body><canvas width="400" height="400" id="canvas"></canvas></body>
</html>
const canvas = document.querySelector("#canvas");
const ctx = canvas.getContext("2d");
// 获取dpr
const dpr = window.devicePixelRatio;
// 获取画布宽、高
const { width, height } = canvas;
// 画布中心点
const centerX = width / 2;
const centerY = height / 2;
// 总进度 按100计算
const total = 100;
// 雷达图最小半径
const minRadius = 25;
const fontSize = 12;
const count = 5;
// 雷达图 分类数据
const data = [
{ label: "温和稳重", value: 90 },
{ label: "开朗活泼", value: 45 },
{ label: "坚毅果断", value: 62 },
{ label: "谦虚内敛", value: 50 },
{ label: "乐观豁达", value: 66 },
// { label: "充满魅力", value: 70 },
];
// 分类数据长度
const dataLen = data.length;
// 每个点的角度
const angle = (Math.PI * 2) / dataLen;
console.log(ctx);
// 开始绘制
draw();
function draw() {
hd(canvas, ctx);
// 画圆
// ctx.beginPath();
// ctx.arc(centerX, centerY, minRadius * dataLen, 0, Math.PI * 2);
// ctx.lineWidth = 2 * dpr;
// ctx.stroke();
for (let i = 0; i <= count; i++) {
// const r = Math.min(canvas.width, canvas.height)/dataLen * (i / dataLen)
// 根据边的数量进行 比例扩散
const r = minRadius * i;
drawPolygon(r, i);
// 最外层的时候绘制
if (i === count) {
drawLine(r);
drawRegion(r);
drawPercentText(r);
drawCategoryText(r);
}
}
}
// 绘制五边形
function drawPolygon(radius) {
ctx.beginPath();
for (let i = 0; i < data.length; i++) {
// angle * i - Math.PI / 2 这里减去 Math.PI / 2 是为了拉正角度为12点方向
// 因为起点默认是 右边3点钟方向开始,想要拉回12点方向需要 减去1/2圆,也就是Math.PI / 2
const x = centerX + Math.cos(angle * i - Math.PI / 2) * radius;
const y = centerY + Math.sin(angle * i - Math.PI / 2) * radius;
// const x = centerX + Math.cos(angle * i) * radius;
// const y = centerY + Math.sin(angle * i) * radius;
ctx.lineTo(x, y);
}
ctx.closePath();
// 填充颜色
// ctx.fillStyle = "rgba(255,71,80,0.2)";
// ctx.fill();
// 绘制边框
ctx.strokeStyle = "#BEBDC3";
ctx.lineWidth = 2;
ctx.stroke();
}
// 绘制连线
function drawLine(radius) {
ctx.beginPath();
for (let i = 0; i < data.length; i++) {
// 拉回12点方向
const x = centerX + Math.cos(angle * i - Math.PI / 2) * radius;
const y = centerY + Math.sin(angle * i - Math.PI / 2) * radius;
ctx.moveTo(centerX, centerY);
ctx.lineTo(x, y);
}
ctx.closePath();
ctx.stroke();
}
// 绘制数据点的覆盖区域
function drawRegion(radius) {
const points = [];
for (let i = 0; i < data.length; i++) {
const ratio = data[i].value / total;
console.log("ratio--", radius, ratio);
// 拉回12点方向
const x = centerX + Math.cos(angle * i - Math.PI / 2) * radius * ratio;
const y = centerY + Math.sin(angle * i - Math.PI / 2) * radius * ratio;
points.push({ x, y });
}
// 绘制面
ctx.beginPath();
for (let i = 0; i < points.length; i++) {
const { x, y } = points[i];
ctx.lineTo(x, y);
}
ctx.closePath();
ctx.fillStyle = "rgba(255,71,80,0.5)";
ctx.fill();
// 绘制点
ctx.beginPath();
for (let i = 0; i < points.length; i++) {
const { x, y } = points[i];
ctx.moveTo(centerX, centerY);
ctx.arc(x, y, 4, 0, 2 * Math.PI);
}
ctx.closePath();
ctx.fillStyle = "#FF4750";
ctx.fill();
}
// 绘制百分比
function drawPercentText(radius) {
ctx.beginPath();
for (let i = 0; i <= count; i++) {
ctx.moveTo(centerX, centerY);
const textX = centerX + Math.cos(-Math.PI / 2) * radius * (i / count);
const textY = centerY + Math.sin(-Math.PI / 2) * radius * (i / count);
ctx.fillStyle = "#777";
ctx.font = `${fontSize}px sans-serif`;
if (i > 0) {
ctx.fillText((i / count) * 100 + "%", textX + 6, textY);
}
}
ctx.closePath();
}
// 绘制分类文字
function drawCategoryText(radius) {
ctx.beginPath();
for (let i = 0; i < data.length; i++) {
ctx.moveTo(centerX, centerY);
const textX =
centerX + Math.cos(angle * i - Math.PI / 2) * (radius + minRadius + 5);
const textY =
centerY + Math.sin(angle * i - Math.PI / 2) * (radius + minRadius + 5);
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "#333";
ctx.font = `${fontSize}px sans-serif`;
ctx.fillText(data[i].label, textX, textY);
// if (i === 0) {
// ctx.fillText(data[i].label, textX, textY + fontSize);
// } else {
// ctx.fillText(data[i].label, textX, textY);
// }
// 优化文字显示距离
// const padding = 5;
// if (angle * i === 0) {
// ctx.fillText(data[i].label, textX, textY + fontSize);
// } else if (angle * i > 0 && angle * i <= Math.PI / 2) {
// ctx.fillText(data[i].label, textX + padding, textY + fontSize);
// } else if (angle * i > Math.PI / 2 && angle * i <= Math.PI) {
// ctx.fillText(data[i].label, textX, textY);
// } else if (angle * i > Math.PI && angle * i <= (Math.PI * 3) / 2) {
// ctx.fillText(data[i].label, textX - padding, textY - padding);
// } else {
// ctx.fillText(data[i].label, textX - padding, textY + fontSize);
// }
}
ctx.closePath();
}
// 画布高清
function hd(canvas, ctx) {
canvas.width = Math.round(width * dpr);
canvas.height = Math.round(height * dpr);
canvas.style.width = width + "px";
canvas.style.height = height + "px";
ctx.scale(dpr, dpr);
return canvas;
}
3. 绘制圆环波浪
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8" /></head>
<style>
body{background-color: #F7F7FA;}
#canvas{border: 1px solid rgba(0, 0, 0, .1);}
</style>
<body>
<canvas width="400" height="400" id="canvas"></canvas>
</body>
<script src="./wave.js"></script>
</html>
class WaveProgress {
constructor(canvasDom) {
if (!canvasDom) throw new Error("未获取到canvas dom元素");
// 获取canvas和上下文
this.canvas = canvasDom;
this.ctx = this.canvas.getContext("2d");
this.width = this.canvas.width;
this.height = this.canvas.height;
// 获取dpr
this.dpr = window.devicePixelRatio;
// 画布中心点
this.centerX = this.canvas.width / 2;
this.centerY = this.canvas.height / 2;
// 圆半径
this.radius = 150;
// 波浪的进度
this.progress = 0.5;
// 配置
this.wave1 = {
// 振幅
amplitude: 12,
// 波长
waveLen: 200,
// 速度,速度越大越快
speed: 0.015,
// x轴的相对位移
phase: 0,
};
this.wave2 = {
amplitude: 15,
waveLen: 300,
speed: 0.03,
phase: Math.PI,
};
}
init() {
// 高清
this.hd();
// 波浪进度
this.drawWaveProgress();
// 执行动画
requestAnimationFrame(() => this.init());
}
// 画底圆
drawCircle() {
const { centerX, centerY, radius, dpr, ctx } = this;
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
ctx.lineWidth = 2 * dpr;
ctx.fillStyle = "rgb(21 138 234 / 40%)";
ctx.fill();
// ctx.stroke();
ctx.clip();
}
// 画波浪
drawWave(options) {
const { amplitude, waveLen, phase } = options;
const { centerX, centerY, radius, width, height, ctx, progress } = this;
ctx.beginPath();
// 根据canvas的位置确认x坐标
// for (let x = 0; x < width; x++) {
// 根据圆的位置确认x坐标
for (let x = centerX - radius; x <= centerX + radius; x++) {
// 偏移修复,当进度为0或1时的情况
let offset = 0;
if (progress <= 0) {
offset = amplitude;
} else if (progress >= 1) {
offset = -amplitude;
}
const offsetY = radius - progress * radius * 2 + offset;
const y =
amplitude * Math.sin((x / waveLen) * Math.PI * 2 - phase) +
centerY +
offsetY;
ctx.lineTo(x, y);
}
ctx.lineTo(width, height);
ctx.lineTo(0, height);
ctx.closePath();
ctx.fillStyle = this.createGradient();
ctx.fill();
}
// 画波浪进度
drawWaveProgress() {
const { width, height, ctx } = this;
// 每次执行动画清除画布
ctx.clearRect(0, 0, width, height);
this.drawCircle();
this.wave1.phase += this.wave1.speed;
this.wave2.phase += this.wave2.speed;
this.drawWave(this.wave1);
this.drawWave(this.wave2);
}
// 填充渐变色
createGradient() {
const { centerX, centerY, radius, ctx } = this;
//创建渐变对象
const gradient = ctx.createLinearGradient(
centerX - radius,
centerY,
centerX + radius,
centerY
);
//颜色断点
gradient.addColorStop(0, "rgba(255,205,160,.5)");
gradient.addColorStop(1, "rgba(255,71,80,.5)");
return gradient;
}
// 画布高清
hd() {
const { canvas, width, height, dpr, ctx } = this;
canvas.width = Math.round(width * dpr);
canvas.height = Math.round(height * dpr);
canvas.style.width = width + "px";
canvas.style.height = height + "px";
ctx.scale(dpr, dpr);
}
}
const wave = new WaveProgress(document.querySelector("#canvas"));
wave.init();