canvas练习

55 阅读5分钟

记录一些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);
}

效果如下图:

image.png

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;
}

image.png

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();

image.png

相关链接

缓动函数

canvas