简单记录 Canvas 中绘制椭圆形

虽然可以直接用 CanvasRenderingContext2D.ellipse() (Canvas 2D API 添加椭圆路径的方法) 方法去绘制。但因为考虑到浏览器兼容性的问题,所以还需要有个备选方案。

Canvas中绘制椭圆的方法有压缩法,计算法,贝塞尔曲线法等多种方式,下面代码中用的是最简单的压缩法。

import { useEffect, useRef } from 'react';

export default function GroupCanvas() {
  const ref = useRef(null);

  useEffect(() => {
    if (!ref.current.getContext) return;

    let ctx = ref.current.getContext('2d');
    ctx.strokeStyle = '#289FD9';
    ctx.lineWidth = 1;
    ellipse(75, 120, 37, 25, 0, 0, Math.PI * 2);

    function ellipse(
      x,
      y,
      radiusX,
      radiusY,
      rotation,
      startAngle,
      endAngle,
      anticlockwise,
    ) {
      ctx.beginPath();
      if (ctx.ellipse) {
        ctx.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise);
      } else {
        let r = radiusX > radiusY ? radiusX : radiusY; //用大的数为半径
        let scaleX = radiusX / r; //计算缩放的x轴比例
        let scaleY = radiusY / r; //计算缩放的y轴比例
        ctx.save(); //保存当前环境的状态。
        ctx.translate(x, y); //移动到圆心位置
        ctx.rotate(rotation); //进行旋转
        ctx.scale(scaleX, scaleY); //进行缩放
        ctx.arc(0, 0, r, startAngle, endAngle, anticlockwise); //绘制圆形
        ctx.restore(); //返回之前保存过的路径状态和属性。
      }
      ctx.stroke();
    }
  }, []);
  return <canvas ref={ref}></canvas>;
}

最后下面是我需要用到椭圆形的demo。可通过下面配置项,动态生成。

import { useEffect, useRef } from 'react';

export default function GroupCanvas(props) {
  const ref = useRef(null);

  useEffect(() => {
    if (!ref.current.getContext) return;

    let {
      groupSum,
      circleCenterX,
      circleCenterY,
      radiusX,
      radiusY,
      textInCircle,
      firstFontYInCircleY,
      fontSpacing,
      curveTopColor,
      curveBottomColor,
    } = { ...props };

    let ctx = ref.current.getContext('2d');
    //清空canvas
    ctx.clearRect(0, 0, ref.current.width, ref.current.height);

    ctx.fillStyle = '#FF8D02';
    ctx.textAlign = 'center';
    ctx.font = '400 16px SourceHanSansCN';
    ctx.lineWidth = 1;

    for (let i = 0; i < groupSum; i++) {
      //绘制椭圆
      ellipse(
        ctx,
        circleCenterX[i],
        circleCenterY,
        radiusX,
        radiusY,
        0,
        0,
        Math.PI * 2,
      );
      // 绘制椭圆内的字
      for (let t = 0; t < textInCircle[i].length; t++) {
        ctx.fillText(
          textInCircle[i][t],
          circleCenterX[i],
          circleCenterY - radiusY + firstFontYInCircleY + fontSpacing * t,
        );
      }

      // 绘制上曲线
      if (i === groupSum - 1) {
        let curveRadiusX = (circleCenterX[groupSum - 1] - circleCenterX[0]) / 2;
        ellipse(
          ctx,
          circleCenterX[0] + curveRadiusX,
          circleCenterY - radiusY,
          curveRadiusX,
          80,
          0,
          0,
          Math.PI,
          true,
        );
        trilateral(
          ctx,
          { x: circleCenterX[0], y: circleCenterY - radiusY },
          'bottom',
          '#289FD9',
          15,
        );
        trilateral(
          ctx,
          {
            x: circleCenterX[groupSum - 1],
            y: circleCenterY - radiusY,
          },
          'bottom',
          '#289FD9',
          345,
        );
        //曲线上字
        curveOnWord(
          ctx,
          '10%(92112)',
          circleCenterX[0] + curveRadiusX,
          circleCenterY - radiusY - 80 - 10,
        );
      } else if (groupSum > 2) {
        let curveRadiusX = (circleCenterX[i + 1] - circleCenterX[i]) / 2;

        ellipse(
          ctx,
          circleCenterX[i] + curveRadiusX,
          circleCenterY - radiusY,
          curveRadiusX,
          18,
          0,
          0,
          Math.PI,
          true,
          curveTopColor[i],
        );
        trilateral(
          ctx,
          { x: circleCenterX[i], y: circleCenterY - radiusY },
          'bottom',
          curveTopColor[i],
          45,
        );
        trilateral(
          ctx,
          {
            x: circleCenterX[i + 1],
            y: circleCenterY - radiusY,
          },
          'bottom',
          curveTopColor[i],
          315,
        );

        //曲线上字
        curveOnWord(
          ctx,
          '10%(92112)',
          circleCenterX[i] + curveRadiusX,
          circleCenterY - radiusY - 18 - 10,
        );
      }
      //下曲线
      if (groupSum > 3 && i >= 2) {
        ellipse(
          ctx,
          circleCenterX[i - 1],
          circleCenterY + radiusY,
          (circleCenterX[i] - circleCenterX[i - 2]) / 2,
          35,
          0,
          0,
          Math.PI,
          false,
          curveBottomColor[i - 2],
        );
        trilateral(
          ctx,
          { x: circleCenterX[i - 2], y: circleCenterY + radiusY },
          'top',
          curveBottomColor[i - 2],
          -35,
        );
        trilateral(
          ctx,
          { x: circleCenterX[i], y: circleCenterY + radiusY },
          'top',
          curveBottomColor[i - 2],
          35,
        );

        //曲线上字
        curveOnWord(
          ctx,
          '10%(92112)',
          circleCenterX[i - 1],
          circleCenterY + radiusY + 35+20, 
        );
      }
    }
  }, [props.groupSum]);

  //曲线上的字体
  function curveOnWord(ctx, content, x, y) {
    ctx.save();
    ctx.font = '400 12px SourceHanSansCN';
    ctx.fillStyle = '#fff';
    ctx.fillText(content, x, y);
    ctx.restore();
  }

  //绘制三角形
  function trilateral(ctx, where, direction = 'top', color, rotate = 0) {
    ctx.save();
    ctx.beginPath();

    ctx.moveTo(where.x, where.y);
    ctx.translate(where.x, where.y);
    ctx.rotate((rotate * Math.PI) / 180);
    if (direction === 'top') {
      ctx.lineTo(-5, +9);
      ctx.lineTo(+5, +9);
    } else {
      ctx.lineTo(-5, -9);
      ctx.lineTo(+5, -9);
    }

    if (color) {
      ctx.fillStyle = color;
    } else {
      ctx.fillStyle = '#289FD9';
    }
    ctx.fill();

    ctx.restore();
  }
  //绘制椭圆曲线
  function ellipse(
    ctx,
    x,
    y,
    radiusX,
    radiusY,
    rotation,
    startAngle,
    endAngle,
    anticlockwise,
    color = '#289FD9',
  ) {
    ctx.strokeStyle = color;
    ctx.save();
    ctx.beginPath();

    if (ctx.ellipse) {
      ctx.ellipse(
        x,
        y,
        radiusX,
        radiusY,
        rotation,
        startAngle,
        endAngle,
        anticlockwise,
      );
      ctx.stroke();
    } else {
      let r = radiusX > radiusY ? radiusX : radiusY;
      let scaleX = radiusX / r;
      let scaleY = radiusY / r;
      ctx.translate(x, y);
      ctx.rotate(rotation);
      ctx.scale(scaleX, scaleY);
      ctx.arc(0, 0, r, startAngle, endAngle, anticlockwise);
    }
    ctx.stroke();
    ctx.restore();
  }

  return (
    <canvas
      ref={ref}
      width={160 + props.groupSum * 150 + (props.groupSum - 1) * 60}
      height={386}
      style={{
        ...props.outStyle,
      }}
    ></canvas>
  );
}
GroupCanvas.defaultProps = {
  //椭圆的xy坐标
  circleCenterX: [155, 367, 579, 791, 1003],
  circleCenterY: 216,
  //椭圆xy半径
  radiusX: 75,
  radiusY: 50,
  //椭圆内的字
  textInCircle: [
    ['10%', '(10025)', 'A区团'],
    ['10%', '(10025)', 'A区团'],
    ['10%', '(10025)', 'A区团'],
    ['10%', '(10025)', 'A区团'],
    ['10%', '(10025)', 'A区团'],
  ],
  //椭圆内第一个字距离圆顶部距离
  firstFontYInCircleY: 30,
  //椭圆内字体间距
  fontSpacing: 25,
  curveTopColor: ['#D99E28', '#BC28D9', '#D92828', '#2928D9'],
  curveBottomColor: ['#D9C628', '#289FD9', '#D92828'],
};

参考链接:
https://blog.csdn.net/gao_xu_520/article/details/58588020
https://www.cnblogs.com/flybeijing/p/canvas_ellipse.html
https://www.cnblogs.com/fangsmile/p/9530226.html

本文章由javascript技术分享原创和收集

发表评论 (审核通过后显示评论):