总结记录写 Canvas 动画后的几个点

前言

正好之前写了篇文章(Canvas 写的酷炫动画代码分析),所以对 Canvas 动画大概的有了了解,揭开了神秘面纱。
动画的解释是指采用逐帧拍摄对象并连续播放而形成运动的影像技术(因为视觉残像所造成)。所以 Canvas 动画的原理也是一样,通过调用 requestAnimationFrame 在下一帧前,重新绘制。

代码量比较多,代码直接放在文章最后面了。

Canvas 绘制后的清晰度问题

Canvas 不是矢量图,是像图片一样的位图模式。而不同显示器之间像素大小的比率(物理像素分辨率与 CSS 像素分辨率之比)不同,也就是说浏览器使用多少屏幕实际像素来绘制单个CSS像素是不同。可以通过 devicePixelRatio 去获取到设备像素比。
在 Canvas context 中也存在一个 backingStorePixelRatio 的属性,该属性的值决定了浏览器在渲染canvas之前会用几个像素来存储画布信息。

    // 屏幕的设备像素比
    var devicePixelRatio = window.devicePixelRatio || 1;
    // 浏览器在渲染canvas之前存储画布信息的像素比
    var backingStoreRatio =
      ctx.webkitBackingStorePixelRatio ||
      ctx.mozBackingStorePixelRatio ||
      ctx.msBackingStorePixelRatio ||
      ctx.oBackingStorePixelRatio ||
      ctx.backingStorePixelRatio ||
      1;
    // canvas的实际渲染倍率
    var ratio = devicePixelRatio / backingStoreRatio;

    canvas.style.width = canvas.width + "px";
    canvas.style.height = canvas.height + "px";
    canvas.width = canvas.width * ratio;
    canvas.height = canvas.height * ratio;
    ctx.scale(ratio, ratio);
椭圆

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

ellipse 的浏览器兼容性

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

   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();
    }
其他

我里面写了2个简单的动画,一个是圆逐渐扩大透明(每一帧改变填充圆的大小和透明度),一个是曲线上有一个简单动效(每一帧弧线上加的曲线绘制在下一个位置)。总的来讲,对于了解 Canvas 的人来说,这些是很简单的。

  • 还有个功能,需要点击对应的圆时,圆变色,相关联的曲线也变色,有个曲线上的简单动效。

我是通过外层再套个父组件,根据鼠标点击的位置,去判断的,里面涉及到一个 children 比较少用到的一个点。

<props.children.type {...props.children.props} chestnutProps={}></props.children.type>

{React.cloneElement(props.children, { chestnutProps: [] })} 

可以看下这里

代码
import { useState, useEffect, useRef } from "react";

function GroupCanvas(props) {
  const ref = useRef(null);
  const nowCanves = useRef(null);
  //透明度值
  const transparencValue = useRef(1);
  //保存requestAnimationFrame回调的ID
  const circulationId = useRef("");
  //高亮时动态曲线的移动进度
  const ledRate = useRef(0);
  const speed = 0.03, //速度
    long = 0.3; //长度

  useEffect(() => {
    if (!ref.current.getContext) return;
    let ctx = ref.current.getContext("2d");

    // 屏幕的设备像素比
    var devicePixelRatio = window.devicePixelRatio || 1;
    // 浏览器在渲染canvas之前存储画布信息的像素比
    var backingStoreRatio =
      ctx.webkitBackingStorePixelRatio ||
      ctx.mozBackingStorePixelRatio ||
      ctx.msBackingStorePixelRatio ||
      ctx.oBackingStorePixelRatio ||
      ctx.backingStorePixelRatio ||
      1;
    // canvas的实际渲染倍率
    var ratio = devicePixelRatio / backingStoreRatio;

    ref.current.style.width = ref.current.width + "px";
    ref.current.style.height = ref.current.height + "px";
    ref.current.width = ref.current.width * ratio;
    ref.current.height = ref.current.height * ratio;
    ctx.scale(ratio, ratio);
    nowCanves.current = ctx;
  }, []);

  useEffect(() => {
    allDraw();

    return () => {
    window.cancelAnimationFrame(circulationId.current);
    };
  }, [props.textInCircle, props.selects]);

  function allDraw() {
    let {
      groupSum,
      circleCenterX,
      circleCenterY,
      textInCircle,
      firstFontYInCircleY,
      fontSpacing,
      maxRound,
      selects,
    } = { ...props };
    //每帧透明度进行变化
    if (transparencValue.current <= 0.05) {
      transparencValue.current = 1;
    } else {
      transparencValue.current -= 0.01;
    }
    let ctx = nowCanves.current;
    //高亮时动态曲线的角度
    let startPlace = speed * ledRate.current * Math.PI,
      endPlace = long + speed * ledRate.current * Math.PI;
    if (endPlace > Math.PI) {
      ledRate.current = 0;
    } else {
      ledRate.current += 1;
    }

    //清空canvas
    ctx.clearRect(0, 0, ref.current.width, ref.current.height);
    ctx.textAlign = "center";
    ctx.font = "400 12px SourceHanSansCN";

    for (let i = 0; i < groupSum; i++) {
      let circleName = ["A", "B", "C", "D", "E"];
      //绘制圆形
      roundness(
        ctx,
        circleCenterX[i],
        circleCenterY,
        selects.includes(circleName[i])
      );

      // 绘制圆内的字
      ctx.save();
      ctx.fillStyle = "#1D1D1D";
      for (let t = 0; t < textInCircle[i].length; t++) {
        //字体长度太长,省略号
        if (t === 0 && textInCircle[i][0].length > 3) {
          let newSte = textInCircle[i][0].slice(0, 3);
          newSte += "...";
          ctx.fillText(
            newSte,
            circleCenterX[i],
            circleCenterY - maxRound + firstFontYInCircleY + fontSpacing * t
          );
        } else {
          ctx.fillText(
            textInCircle[i][t],
            circleCenterX[i],
            circleCenterY - maxRound + firstFontYInCircleY + fontSpacing * t
          );
        }
      }
      ctx.restore();

      // 绘制上曲线
      if (i === groupSum - 1 && groupSum > 3) {
        let isbright = false;
        if (
          selects.includes("A") ||
          selects.includes(String.fromCharCode(64 + groupSum))
        ) {
          isbright = true;
        }

        //最长上曲线
        let curveRadiusX = (circleCenterX[groupSum - 1] - circleCenterX[0]) / 2;
        ellipse(
          ctx,
          circleCenterX[0] + curveRadiusX,
          circleCenterY - maxRound,
          curveRadiusX,
          80,
          0,
          -0.03 * Math.PI,
          1.03 * Math.PI,
          true,
          isbright ? "#DDDD6F" : "rgba(201,224,255,0.3)"
        );
        //动曲线
        if (isbright) {
          dynamicCurve(
            ctx,
            circleCenterX[0] + curveRadiusX,
            circleCenterY - maxRound,
            curveRadiusX,
            80,
            0,
            -startPlace,
            -endPlace,
            true,
            "#fff"
          );
        }

        trilateral(
          ctx,
          { x: circleCenterX[0], y: circleCenterY - maxRound },
          "bottom",
          15,
          isbright ? "#DDDD6F" : "rgba(201,224,255,0.3)"
        );
        trilateral(
          ctx,
          {
            x: circleCenterX[groupSum - 1],
            y: circleCenterY - maxRound,
          },
          "bottom",
          345,
          isbright ? "#DDDD6F" : "rgba(201,224,255,0.3)"
        );
      } else if (i < groupSum - 1) {
        let curveRadiusX = (circleCenterX[i + 1] - circleCenterX[i]) / 2,
          curveName = ["AB", "BC", "CD", "DE"],
          isbright = false;

        for (let str of selects) {
          if (curveName[i].includes(str)) {
            isbright = true;
            break;
          }
        }

        ellipse(
          ctx,
          circleCenterX[i] + curveRadiusX,
          circleCenterY - maxRound,
          curveRadiusX,
          18,
          0,
          -0.11 * Math.PI,
          1.11 * Math.PI,
          true,
          isbright ? "#DDDD6F" : "rgba(201,224,255,0.3)"
        );
        //动曲线
        if (isbright) {
          dynamicCurve(
            ctx,
            circleCenterX[i] + curveRadiusX,
            circleCenterY - maxRound,
            curveRadiusX,
            18,
            0,
            -startPlace,
            -endPlace,
            true,
            "#fff"
          );
        }
        trilateral(
          ctx,
          { x: circleCenterX[i], y: circleCenterY - maxRound },
          "bottom",
          45,
          isbright ? "#DDDD6F" : "rgba(201,224,255,0.3)"
        );
        trilateral(
          ctx,
          {
            x: circleCenterX[i + 1],
            y: circleCenterY - maxRound,
          },
          "bottom",
          315,
          isbright ? "#DDDD6F" : "rgba(201,224,255,0.3)"
        );
      }
      //下曲线
      if (groupSum > 2 && i >= 2) {
        let curveName = ["AC", "BD", "CE"],
          isbright = false;

        for (let str of selects) {
          if (curveName[i - 2].includes(str)) {
            isbright = true;
            break;
          }
        }

        ellipse(
          ctx,
          circleCenterX[i - 1],
          circleCenterY + maxRound,
          (circleCenterX[i] - circleCenterX[i - 2]) / 2,
          35,
          0,
          0.07 * Math.PI,
          0.93 * Math.PI,
          false,
          isbright ? "#DDDD6F" : "rgba(201,224,255,0.3)"
        );
        //动曲线
        if (isbright) {
          dynamicCurve(
            ctx,
            circleCenterX[i - 1],
            circleCenterY + maxRound,
            (circleCenterX[i] - circleCenterX[i - 2]) / 2,
            35,
            0,
            startPlace,
            endPlace,
            false,
            "#fff"
          );
        }
        trilateral(
          ctx,
          { x: circleCenterX[i - 2], y: circleCenterY + maxRound },
          "top",
          -35,
          isbright ? "#DDDD6F" : "rgba(201,224,255,0.3)"
        );
        trilateral(
          ctx,
          { x: circleCenterX[i], y: circleCenterY + maxRound },
          "top",
          35,
          isbright ? "#DDDD6F" : "rgba(201,224,255,0.3)"
        );
      }
      // 当组团有5个时
      if (groupSum === 5 && i >= 3) {
        let curveName = ["AD", "BE"],
          isbright = false;

        for (let str of selects) {
          if (curveName[i - 3].includes(str)) {
            isbright = true;
            break;
          }
        }

        let curveRadiusX = (circleCenterX[i] - circleCenterX[i - 3]) / 2;
        ellipse(
          ctx,
          circleCenterX[i] - curveRadiusX,
          circleCenterY + maxRound,
          curveRadiusX,
          75,
          0,
          0.03 * Math.PI,
          0.97 * Math.PI,
          false,
          isbright ? "#DDDD6F" : "rgba(201,224,255,0.3)"
        );

        //动曲线
        if (isbright) {
          dynamicCurve(
            ctx,
            circleCenterX[i] - curveRadiusX,
            circleCenterY + maxRound,
            curveRadiusX,
            75,
            0,
            startPlace,
            endPlace,
            false,
            "#fff"
          );
        }

        trilateral(
          ctx,
          { x: circleCenterX[i - 3], y: circleCenterY + maxRound },
          "top",
          -15,
          isbright ? "#DDDD6F" : "rgba(201,224,255,0.3)"
        );
        trilateral(
          ctx,
          { x: circleCenterX[i], y: circleCenterY + maxRound },
          "top",
          15,
          isbright ? "#DDDD6F" : "rgba(201,224,255,0.3)"
        );
      }
    }
    //循环
    circulationId.current = window.requestAnimationFrame(allDraw);
  }

  //绘制三角形
  function trilateral(
    ctx,
    where,
    direction = "top",
    rotate = 0,
    color = "rgba(201,224,255,0.3)"
  ) {
    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);
    }

    ctx.fillStyle = color;
    ctx.fill();

    ctx.restore();
  }
  //绘制椭圆曲线
  function ellipse(
    ctx,
    x,
    y,
    radiusX,
    radiusY,
    rotation,
    startAngle,
    endAngle,
    anticlockwise,
    color = "rgba(201,224,255,0.3)"
  ) {
    ctx.save();
    ctx.strokeStyle = color;
    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();
  }

  //绘制动圆
  function roundness(ctx, x, y, isbright) {
    ctx.save();

    let colors = isbright ? ["#DDDD6F", "#DD8B6F"] : ["#84B8FF", "#AACEFF"],
      tbArc = isbright
        ? ["rgba(221,221,111,1)", "rgba(221,221,111,0)"]
        : ["rgba(201, 224, 255, 1)", "rgba(201, 224, 255, 0)"];
    let grd = ctx.createLinearGradient(
      x,
      y - props.maxRound,
      x,
      y + props.maxRound
    );
    grd.addColorStop(0, colors[0]);
    grd.addColorStop(1, colors[1]);
    ctx.fillStyle = grd;

    //背景圆
    ctx.beginPath();
    ctx.globalAlpha = 0.05;
    ctx.arc(x, y, props.maxRound, 0, 2 * Math.PI);
    ctx.fill();
    //逐渐放大圆
    ctx.beginPath();
    ctx.globalAlpha = transparencValue.current - 0.01;
    ctx.arc(
      x,
      y,
      props.initialRound +
        (props.maxRound - props.initialRound) * (1 - transparencValue.current),
      0,
      2 * Math.PI
    );
    ctx.fill();
    // 中间基础圆
    ctx.beginPath();
    ctx.globalAlpha = 1;
    ctx.arc(x, y, props.initialRound, 0, 2 * Math.PI);
    ctx.fill();

    // 上下曲线
    ctx.beginPath();
    let grd2 = ctx.createLinearGradient(x, y - props.maxRound, x, y);
    grd2.addColorStop(0, tbArc[0]);
    grd2.addColorStop(0.4, tbArc[1]);
    ctx.strokeStyle = grd2;
    ctx.arc(x, y, props.maxRound, 0.2 * Math.PI, 0.8 * Math.PI, true);
    ctx.stroke();

    ctx.beginPath();
    let grd3 = ctx.createLinearGradient(x, y + props.maxRound, x, y);
    grd3.addColorStop(0, tbArc[0]);
    grd3.addColorStop(0.4, tbArc[1]);
    ctx.strokeStyle = grd3;
    ctx.arc(x, y, props.maxRound, 0.2 * Math.PI, 0.8 * Math.PI);
    ctx.stroke();

    ctx.restore();
  }

  //增加的动态曲线
  function dynamicCurve() {
    arguments[0].save();
    arguments[0].globalCompositeOperation = "source-atop";
    ellipse(...arguments);
    arguments[0].restore();
  }

  return (
    <canvas
      ref={ref}
      width={160 + props.groupSum * 150 + (props.groupSum - 1) * 60}
      height={386}
      style={{
        ...props.outStyle,
      }}
    ></canvas>
  );
}

export default React.memo(GroupCanvas);

GroupCanvas.defaultProps = {
  //内圆半径
  initialRound: 25,
  //背景圆半径
  maxRound: 55,
  //圆的xy坐标
  circleCenterX: [155, 367, 579, 791, 1003],
  circleCenterY: 200,
  //圆内的字
  textInCircle: [
    ["A区团", "10%"],
    ["A区团", "10%"],
    ["A区团", "10%"],
    ["A区团", "10%"],
    ["A区团", "10%"],
  ],
  //圆内第一个字距离圆顶部距离
  firstFontYInCircleY: 55,
  //圆内字体间距
  fontSpacing: 15,
  //数量目前最多限定5个
  groupSum: 5,
  //被选中高亮
  selects: [],
};

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容