【附源码+在线预览】我用四种方式实现箭头动画(css3+canvas+svg+js)讲解超详细

大家好!我是猫小白,本期给大家带来一个简单的实战demo,希望大家食用得开心,有收获。

首先声明,此demo存粹为了学习其中的动画知识,请不要用于真实的场景,后果自负
首先声明,此demo存粹为了学习其中的动画知识,请不要用于真实的场景,后果自负
首先声明,此demo存粹为了学习其中的动画知识,请不要用于真实的场景,后果自负
此项目基于某健康通,其它地区的场所码不知道是否有类似的动画,如果没有也没关系,可以学习下是如何实现的。
为啥子要做这个呢?那是因为有一天我出去买菜,回来保安叔叔恩是要我扫一下场所码,作为一个5星好公民,肯定是非常配合的掏出手机开始扫 。叮~,屏幕上出现一个不挺放大缩小的箭头。我就想,要不用代码来实现一下这个动画吧。
v2dc289a899a329b58eda170da213709f9_720w.jpg

于是就有了这篇文章,我一共是实现了4种方法:

  1. 第一种,css,零js代码(最简单)
  2. 第二种,svg,零js代码 (较简单)
  3. 第三种,js+dom (较复杂)
  4. 第四种,js+canvas (较复杂)

附全文源码地址:github源码demo在线预览地址

微信图片_20220503174339.jpg
请拉下代码,边浏览文字边打开编辑器一起操作哦~
接下来,我们一个一个来分析如何实现的。

准备工作

废话)首先我们假装出去买个菜(未解封的同胞除外例如上海的朋友),回来的时候去门卫那里故作镇定,连贯的掏出手机打开ZFB扫一下,然后给门卫亮一下,不要退出界面,淡定回家。
回到家后,拿出手机,打开刚刚的界面,截一个图。
加入html中,作为div容器的背景图片,然后我们创建一个白色的框把中间的圆圈和勾遮住,因为我们要自己实现其中的动画效果。

遮板的css代码:

/**遮板**/
.shutter {
  position: absolute;
  display: block;
  width: 120px;
  height: 120px;
  background-color: #fff;
  top: 206px;
  left: 150px;
}

核心html代码结构如下,:

<body>
    <div class="app">
      <div class="content">
        <!--遮板把原来的箭头用白色背景遮住,自己实现箭头-->
        <div class="shutter"></div>
        <!--自己实现的箭头和圆-->
        <div class="circle"></div>
      </div>
    </div>
  </body>

只是列举关键的代码,完整代码请到我的github仓库查看。
准备工作完成,开始正片!

第一种,用css3动画,零js代码

使用css3animation动画,可以很容易实现这样的效果,不用一行js。但关键是要看明白我们目标动画是由哪些部分组成的,动画是怎么变化的。所以先再次看看这个gif,注意其中动画部分。
不难发现:

  • 圆圈是由小到大,再由大到小
  • 内圈放大到一定程度时,外圈(扩散部分)出现,并且半径不停增大,当内圈放到最大时,扩散圈消失
  • 内部的勾也会一起放大缩小,频率和内圈相同
    根据上面的规律,我们先实现一个内圈(绿色圈部分)

内部圈css代码:

/**内部圆圈**/
.circle {
  position: absolute;
  z-index: 9;
  width: var(--circle-size);
  height: var(--circle-size);
  top: 218px;
  left: 165px;
  background-color: var(--theme-green);
  /* background-color: red; */
  border-radius: 45px;
  animation: circleMove 0.7s alternate infinite cubic-bezier(1, 1, 1, 1);
}

–circle-size、–theme-green是我们在开头定义的变量,方便后续使用。

:root {
  --theme-green: #4ba776; /**主题绿色**/
  --circle-size: 90px; /**圆圈大小 宽高一样**/
}

内部圈画好了,还差一个白色的"√",为了简单我直接使用了"√"这个字符串,加入伪类元素的content中。你们也可以用图片或者svg
"√"的css代码如下:

/**内部的勾,使用`.circle`的`before`伪类**/
.circle::before {
  z-index: 2;//z-index要比外圈(阔散圈)大,否则要被背景覆盖
  position: absolute;
  display: block;
  content: "✔";
  font-size: 63px;
  color: #fff;
  left: 18px;
  top: -1px;
}

现在我们按照内圈相同的颜色和大小,画一个外部圈(扩散圈)。
参考上面的"√"的实现,我们用after伪类来实现:

/**动画时外部渐变圈*/
.circle::after {
  z-index: 1;//z-index要比内部的勾小,否则要覆盖掉勾
  position: absolute;
  display: block;
  content: "";
  width: calc(var(--circle-size));
  height: calc(var(--circle-size));
  box-sizing: border-box;
  border-radius: 50%;
  background-color: #4ba776;
  transform: scale(1.2);
  /**外圈动画 一轮时间是内圈的2倍 内部圈放大0.7s+缩小0.7s=1.4s  infinite无限重复**/
  /* animation: outCircleMove 1.4s infinite cubic-bezier(1, 1, 1, 1); */
}

好了,我们开始加入动画
内圈加入放大缩小动画,在.circle中计入animation

/**内部圆圈**/
.circle {
  position: absolute;
  z-index: 9;
  width: var(--circle-size);
  height: var(--circle-size);
  top: 218px;
  left: 165px;
  background-color: var(--theme-green);
  /* background-color: red; */
  border-radius: 45px;
  /*加入放大缩小动画  alternate:轮流播放,放大再放小  infinite:无限播放  ease-in-out:变化函数*/
  animation: circleMove 0.7s alternate infinite ease-in-out;
}
/**定义放大缩小动画,最大放大到1.2倍**/
@keyframes circleMove {
  0% {
    transform: scale(1);
  }
  100% {
    transform: scale(1.2);
  }
}

解读:时间设置为0.7s,我们定义了circleMove 动画,意思是在0秒的时候圆圈保持1倍大小,在0.1~0.7s圆圈逐渐变为1.2倍大小。变化的快慢是由animation-timing-function参数决定的,文中设置的ease-in-out是进入和退出较慢,中间速度较快的一种变化曲线。
我们可以打开开发者工具,点击animation属性的曲线图标,查看和选择想要的动画曲线
动画曲线.png

好,内圈动画已经完成

perfect!现在还差外圈动画~

让我们回顾一下:外圈动画是要在内圈放大到一定时间外圈才出现,我们暂定1/5,也就是20%的时候显示并且开始放大(放大对应属性:scale);放大后外圈会越来越便透明,直到内圈放到最大,此时完全透明,感觉像是扩散的效果(透明对应属性:opacity)。
知道这些后,我们设置外圈动画如下:
/*外部的渐变圈/

.circle::after {
  z-index: 1;
  position: absolute;
  display: block;
  content: "";
  width: calc(var(--circle-size));
  height: calc(var(--circle-size));
  box-sizing: border-box;
  border-radius: 50%;
  background-color: #4ba776;
  /**外圈动画 一轮时间是内圈的2倍 内部圈放大0.7s+缩小0.7s=1.4s  infinite无限重复**/
  animation: outCircleMove 1.4s infinite ease-in-out;
}
/**定义扩散圈的放大缩小动画**/
@keyframes outCircleMove {
  /**0%~50% 0s~ 0.7s前的时间 此时内圈放大,扩散圈等待时机出现**/
  0% {
    opacity: 0;
  }
  /**10% 此时内圈已经放大一段时间, 扩散全显示opacity设为1,倍率为1 **/
  10% {
    opacity: 1;
    transform: scale(1);
  }
  /**50%到100%是动画0.7s~1.4s的时间 此时内圈缩小,扩散圈变为1.3倍,但是透明度设为0**/
  50%,
  100% {
    transform: scale(1.3);
    opacity: 0;
  }
}

至此就大功告成了

第二种,用svg实现,零js代码 (较简单)

上面了利用css3的动画属性,操作起来非常简单。现在介绍一种用svg动画的方式实现相同的效果。我们知道svg也是有比较丰富的动画特性支持的。
我们简单列举一下svg实现动画的几种方法:

1.set

set元素是svg动画元素中最简单的元素。在经过特定时间间隔后,它只是将属性设置为特定值。因此,形状不会连续进行动画处理,而只是更改属性值一次。

2.animate

<animate>元素通常放置到一个SVG图像元素里面,用来定义这个图像元素的某个属性的动画变化过程。

3.animateMotion

<animateMotion>元素也是放置一个图像元素里面,它可以引用一个事先定义好的动画路径,让图像元素按路径定义的方式运动。

4.animateTransform

动画上一个目标元素变换属性,从而使动画控制平移,缩放,旋转或倾斜。
简单了解过后,我们今天主要用animate来实现动画。

首先,我们还是拿出上面的基础布局,祖传代码(背景+遮罩)

<div class="app">
  <div class="content">
    <!--遮板把原来的箭头用白色背景遮住,自己实现箭头-->
    <div class="shutter"></div>
    <!--自己实现的箭头动画-svg-->
    <svg></svg>
  </div>
</div>
</body>

基本框架的样式和上一节css3动画的样式相同,就不重复粘贴了。

然后我们画一个圆和勾,也就是内部的部分;svg中画圆是用<circle>,于是我们有这样的代码:

<div class="app">
  <div class="content">
    <!--遮板把原来的箭头用白色背景遮住,自己实现箭头-->
    <div class="shutter"></div>
    <!--自己实现的箭头和圆-->
  <svg
    class="circle"
    width="130"
    height="130"
    xmlns="http://www.w3.org/2000/svg"
  >
    <g>
      <circle cx="45" cy="45" r="45" fill="#4ba776"></circle>
    </g>
  </svg>
  </div>
</div>
</body>

继续添加中间的白色"√"。为了简介,我们用<text>文字标签:
添加<text>后的代码如下:

<div class="app">
  <div class="conten ">
    <!--遮板把原来的箭头用白色背景遮住,自己实现箭头-->
    <div class="shutter"></div>
    <!--自己实现的箭头和圆-->
  <svg
    class="circle"
    width="130"
    height="130"
    xmlns="http://www.w3.org/2000/svg"
  >
    <g>
      <circle cx="65" cy="65" r="45" fill="#4ba776"></circle>
      <!--text添加-->
      <text font-size="63" fill="#fff" y="90" x="38">✔</text>
    </g>
  </svg>
  </div>
</div>
</body>

好了,内圈添加好了,我们再添加一个外圈(扩散圈),还是用<circle>,大小和位置都和内圈一模一样,这里就不粘贴代码了。

现在来添加动画,根据上一章节的css3动画,我们需要不停的放大缩小内圈的大小。所以我们通过动画改编圆的半径就ok了。上面介绍了<animate>标签可以动态改变某一属,这里的属性就是半径r:

...其它代码
<circle cx="65" cy="65" r="45" fill="#4ba776">
  <!--添加到circle标签内部,指定attributeName为半径r,values是半径变化的值,我们让它先变大再变小,dur是动画时间 repeatCount:indefinite无限循环-->
  <animate
    attributeName="r"
    values="45;54;45"
    dur="1.4s"
    repeatCount="indefinite"
  />
</circle>
...其它代码

完美!看起来就是那么回事,后面的步骤其实都是依葫芦画瓢。
添加内部"√"的动画,用<animate>改变font-size

<text font-size="63" fill="#fff" y="90" x="38">
  ✔
  <animate
    attributeName="font-size"
    values="63;69;63"
    dur="1.4s"
    repeatCount="indefinite"
  />
</text>

最后,我们添加扩散效果。扩散效果其实分两步,第一步是放大半径,另一步是从不透明到透明的过程。所以我们用<animate>改变半径的同时,改变它的透明度。

...其它代码
<!--扩散圈--->
<circle cx="65" cy="65" r="45" fill="#4ba776">
  <!--设置放大缩小动画--->
  <animate
    attributeName="r"
    values="45;50;65;65;50;45"
    dur="1.4s"
    repeatCount="indefinite"
  />
  <!--设置透明度变化动画--->
  <animate
    attributeName="opacity"
    values="1;1;0.2;0;0;0"
    dur="1.4s"
    repeatCount="indefinite"
  />
</circle>
...其它代码

最终效见github在线预览

第三种,js+dom (较复杂)

前面都是通过css3或者svg自带特性完成的动画效果,下面这种是通过js动态改变圆圈div大小实现的,较为复杂,理解起来需要一定的js基础,在做项目优先选择css3,我们一起来看下是如何实现的。
首先,我们还是拿出上面的基础布局,添加内外圈的div和布局。
关键css部分:

/**外部圆圈**/
.circle,
.outCircle {
  z-index: 1;
  position: absolute;
  width: var(--circle-size);
  height: var(--circle-size);
  top: 218px;
  left: 165px;
  background-color: var(--theme-green);
  /* background-color: red; */
  border-radius: 45px;
}
/**内部的勾**/
.circle::before {
  position: absolute;
  display: block;
  content: "✔";
  font-size: 63px;
  color: #fff;
  left: 18px;
  top: -1px;
}

html部分:

<body>
  <div class="app">
    <div class="content">
      <!--遮板把原来的箭头用白色背景遮住,自己实现箭头-->
      <div class="shutter"></div>
      <!--自己实现的箭头和圆-->
      <div class="circle"></div>
      <div class="outCircle"></div>
    </div>
  </div>
</body>

至此,元素已经画好了,就差动画部分了,简单分析一波:
内圈需要逐渐放大再逐渐放小,我们用一个变量size_inner来代表放大缩小的倍数,STEP_INNER代表内圈递增或者递减的步长,最终设置到style上的transform属性。
外圈也需要有一个放大过程,且放大的速度比内圈快,我们用size_outer来代表放大倍数,STEP_OUT代表外圈递增步长。
外圈需要一个透明度逐渐小的值opacity_out ,透明度变化的步长用OPA_STEP代替,最终设置style上的opacity属性。
动画是持续不断的,所以我们需要不停的执行,先暂时采用setInterval函数;
放大到一定倍数然后再缩小,最大倍数我们用MAX_SCALE常量代表,如果递增到最大然后STEP_INNER设置为负数,意味着开始缩小。

最终的源代码如下:

<script>
  const circle = document.querySelector(".circle");
  const outCircle = document.querySelector(".outCircle");
  let size_inner = 1.0; //内圈的放大倍数
  let size_outer = 1.0; //内圈的放大倍数
  let STEP_INNER = 0.009; //内圈的递增步值
  let STEP_OUT = STEP_INNER + 0.009; //外圈要比内圈扩散倍速大一点
  let opacity_out = 1; //外圈透明度
  const OPA_STEP = 0.03;
  const MAX_SCALE = 1.4;
  function randerInner() {
    //放大过程
    if (STEP_INNER >= 0) {
      if (size_inner <= MAX_SCALE) {
        size_inner += STEP_INNER;
        //控制外圈光环--start
        if (size_inner >= 1.1) {
          //内圈放大到1.1倍 外圈光环才出现,大小和内圈一致,透明度为1
          if (opacity_out === 0) {
            opacity_out = 1;
            size_outer = size_inner;
          }
          size_outer += STEP_OUT;
          opacity_out -= OPA_STEP;//透明度逐渐
        }
        //控制外圈光环--end
      } else {
        //放大到MAX_SCALE倍了 重新缩小
        size_inner = MAX_SCALE;
        STEP_INNER = STEP_INNER * -1; //步长乘以-1,开始方方向变化
        //控制外圈光环-隐藏
        opacity_out = 0;
      }
    } else {
      //缩小过程
      if (size_inner > 1) {
        size_inner += STEP_INNER;
      } else {
        //缩到1倍了 重新放大
        STEP_INNER = STEP_INNER * -1; //步长乘以-1,开始方方向变化
        size_inner = 1;
      }
    }
    circle.style.transform = `scale(${size_inner})`; //设这内圈放大样式
    outCircle.style.transform = `scale(${size_outer})`; //设这内圈放大样式
    outCircle.style.opacity = opacity_out; //设这内圈透明度
  }
  setInterval(() => {
    randerInner();
  }, 17);
</script>

因篇幅有限,不逐一解释各行代码的含义,有不理解的可以一起交流。

最终效见github在线预览
众所周知,因为浏览器的eventloop机制,setInterval往往并不能按照设置的时间执行,在稍微复杂一些的页面中会出现卡顿。固可采用requestAnimationFrameapi代替。
代码如下:

<script>
    const circle = document.querySelector(".circle");
    const outCircle = document.querySelector(".outCircle");
    let size_inner = 1.0; //内圈的放大倍数
    let size_outer = 1.0; //内圈的放大倍数
    let STEP_INNER = 0.009; //内圈的递增步值
    let STEP_OUT = STEP_INNER + 0.009; //外圈要比内圈扩散倍速大一点
    let opacity_out = 1; //外圈透明度
    const OPA_STEP = 0.03;
    const MAX_SCALE = 1.4;
    (function randerInner() {
      //放大过程
      if (STEP_INNER >= 0) {
        if (size_inner <= MAX_SCALE) {
          size_inner += STEP_INNER;
          //控制外圈光环--start
          if (size_inner >= 1.1) {
            //内圈放大到1.2倍 外圈光环才出现大小一致
            if (opacity_out === 0) {
              opacity_out = 1;
              size_outer = size_inner;
            }
            size_outer += STEP_OUT;
            opacity_out -= OPA_STEP;
          }
          //控制外圈光环--end
        } else {
          //放大到MAX_SCALE倍了 重新缩小
          size_inner = MAX_SCALE;
          STEP_INNER = STEP_INNER * -1;

          //控制外圈光环-隐藏
          opacity_out = 0;
        }
      } else {
        //缩小过程
        if (size_inner > 1) {
          size_inner += STEP_INNER;
        } else {
          //缩到1倍了 重新放大
          STEP_INNER = STEP_INNER * -1;
          size_inner = 1;
        }
      }
      circle.style.transform = `scale(${size_inner})`; //设这内圈放大样式
      outCircle.style.transform = `scale(${size_outer})`; //设这内圈放大样式

      outCircle.style.opacity = opacity_out; //设这内圈透明度
      requestAnimationFrame(() => {
        randerInner();
        _test();
      });
    })()
</script>

第四种,js+canvas (较复杂)

经过上面章节的洗礼,canvas版本和js+dom版本非常相似,唯一的不同就是绘画圆或者文字的api不同。
我们分析下要点:

  1. 需要在画布上用arcapi绘制2个圆,一个内圈一个外圈,并且不停改变圆圈的大小或透明度。
  2. 需要用fillText绘制一个字样,并且改变字体大小。
  3. 需要不停执行绘制函数,这里使用流畅的requestAnimationFrameapi。
  4. 每次绘制前,需清空画布clearRect
    其实原理和第三种差不多,都需要用变量记录内部圈变化的大小,到最大值后方向递减。外部圈是需要等到内圈放大到一定倍数(1.1)倍,才显示出来,并且放大速度要比内圈快,随着外圈变大,透明度逐渐减小到0。

我想第四种和第三种其实都有这样的逻辑,为此我花了一点把这个动作进行了封装,采用了一种订阅发布模式来实现。
为了大家浏览我这篇文章的同时,除了能学习到动画知识,也能学习到一些其它的比较重要的js知识。大家应该了解或者听过js的各种模式,什么工厂模式、单列模式、观察者模式、发布订阅模式等等,都是学习到了理论知识。今天我就用这个例子介绍下发布订阅模式是如何在实践中使用。

刚我们分析了canvas实现的方式和js+dom的方式,这两种方式都需要用变量来记录一个值(这里是放大倍数),而且每次执行渲染函数时都要递增一个常量值(成为步长),到达某个最大值(比如1.3倍大小)后开始反向递减(步长乘以-1),然后递减到最初值(1倍大小)。同时还有一个细节,当内圈递增到一定倍数(比如1.1倍)要让外圈开始加入动画,当内圈递增到最大时,外圈要消失(透明度设置为0)

基于这个分析,我们得出封装的类需要设计成这样:

/***
 * 放大缩小数字变化类
 * 根据放大缩小初始值和步长等递增
 * 提供到达某一数值时的回调
 * @params {Number} from 数字初始值
 * @params {Number} to 数字最大值
 * @params {Number} step 数字步长
 * @params {Array} dingNums 达到dingNums的区间触发回调 只再放大轮回触发
 */
export default class MoveCrycle {
  constructor({ from = 0, to = 100, step = 1, dingNums = [] }) {
    this.from = from;
    this.to = to;
    this.step = step;
    this.dingNums = dingNums;

    this.currValue = this.from;
    this._turnNum = 0; //放大缩小轮次 一次放大一次缩小为一轮
    this._event = {
      onchange: [], //每一次变化触发的回调
      onlarge: [], //每次到最大值触发的回调
      onsmall: [], //每次到最小值触发的回调
      onding: [], //每次到dingNums区间值时触发的回调
    };
    this._init();
  }
  _init() {
    //注册放大事件
    //记录轮次 每次放大+1
    this.on("onlarge", function turn() {
      this._turnNum++;
    });
  }
  //开始滚动数字
  jump() {
    if (this.step > 0) {
      //放大过程
      if (this.currValue <= this.to) {
        //递增过程中
        this.currValue += this.step;
        //判断是否在dingNums区间
        if (
          this.currValue >= this.dingNums[0] &&
          this.currValue <= this.dingNums[1]
        ) {
          this._emit("onding", this.currValue, this._turnNum);
        }
      } else {
        this._emit("onsmall", this.currValue);
        this.currValue = this.to;
        this.step *= -1;
      }
    } else {
      //缩小过程
      if (this.currValue > this.from) {
        this.currValue += this.step;
      } else {
        //缩到初始值了 重新放大
        this.step *= -1;
        this.currValue = this.from;
        this._emit("onlarge", this.currValue);
      }
    }
    this._emit("onchange", this.currValue, this.step > 0, this._turnNum);
  }
  //触发事件函数
  _emit(type, ...args) {
    //触发每次变动函数
    this._event[type].forEach((f) => {
      try {
        f.apply({}, args);
      } catch (e) {
        console.error(e);
      }
    });
  }

  //注册回调
  on(type, func) {
    if (
      this._event[type] &&
      !this._event[type].filter((f) => f === func).length
    ) {
      this._event[type].push(func);
    }
  }
  //注销回调
  off(type, func) {
    if (this._event[type]) {
      this._event[type] = this._event[type].filter((f) => f !== func);
    }
  }
}

一看比较长,我们别急慢慢分析:

  1. constructor部分就是接收入参,没有什么好说的。需要注意的是this._init(),初始化时自己也注册一个onlarge事件,来记录每次轮回的次数_turnNum
  2. jump是核心代码,主要管理每一次被执行时的递增或递减状态,同时通过this.emit()函数触发响应的回调事件。放大过程中判断是否到达dingNums的区间,如果在其中则不停触发onding事件,当到达最大值时触发onsmall事件代表即将变小,同时this.step *= -1步长取反方向值。到达最小值时触发onlarge事件,代表即将增大,同时this.step *= -1步长取反方向值。注意jump函数并没有这个类中进行递归调用,是因为把这个调用权交给使用者会更加灵活,后面会介绍如何调用。
  3. _emit函数是触发事件的控制中心,在发布订阅模式中就是发布者,根据传入的type类型找到事件对象中对应的函数数组,遍历执行。用try{} catch(e){}是为了避免在某个回调函数中出错而影响了其它回调函数正常执行。
  4. onoff顾名思义,就是订阅和取消订阅事件回调的入口。on函数把传入的事件类型和函数加入对应的数组中,如果重复添加同一个函数是无效的。off函数接收事件类型和函数,然后数组中过滤掉。这里留给你想像一下,如果用匿名函数注册的事件能取消掉吗?

好了,MoveCrycle类封装好了,回到我们的主题,看下如何在这个箭头动画中使用。

HTML部分

<body>
  <div class="app">
    <div class="content">
      <!--canvas替换箭头的部分-->
      <canvas
        id="canvas"
        class="canvas"
        width="140px"
        height="140px"
      ></canvas>
    </div>
  </div>
</body>

script部分

引入MoveCrycle类,初始化

import MoveCrycle from "./js/MoveCrycle.js";

let cavas = document.getElementById("canvas");
var ctx = cavas.getContext("2d");
//定义的一些常量和变量下面会用到
const R_SMALL = 45;//内圈初始大小
...省略部分代码

//看着里!!!!初始化封装的类
let move = new MoveCrycle({
  from: R_SMALL, //初始值
  to: R_SMALL * 1.35, //最大1.35倍
  step: 0.3, //步长
  dingNums: [R_SMALL * 1.1, R_SMALL * 1.35], //外圈开始出现的区间内圈的1.1倍到1.35倍
});
...

订阅onchange事件,监听每一次变化,然后渲染画布

//订阅数值变化事件,每次变化获取值渲染
move.on("onchange", (value, step) => {
  //绘制内圆
  clear(); //清空画布
  //绘制内圈
  drawInnerCircle(value);
  //绘制外圈
  drawOutCircle();
  //文字放在最后,不然会被上面的圆属性fillStyle覆盖
  drawText(value);
});

订阅onding事件,控制外圈的变大,透明度减小

//再设定的区间触发事件,控制外圈的变大,透明度减小
move.on("onding", (value) => {
  r_out += R_OUT_STEP; //外圈变大
  opa -= OPA_STEP; //外圈变大的同时透明度减小
});

订阅onlarge事件,控制外圈

//动画圈开始变大控制外圈
move.on("onlarge", (value) => {
  opa = 1; //开始变大,透明度为1
  r_out = 45; //外部开始变大时半径回复到初始值
});

订阅onsmall事件,控制外圈透明度为0

move.on("onsmall", (value) => {
  //控制外圈光环-隐藏
  opa = 0;
});

绘制内圈、外圈、文字函数

//绘制内圆
function drawInnerCircle(val) {
  ctx.beginPath();
  ctx.arc(SIZE / 2, SIZE / 2, val, 0, 2 * Math.PI);
  ctx.fillStyle = `rgba(${COLOR}, 1)`;
  ctx.fill();
}
//绘制外圈圆 变大的同时透明度下降
function drawOutCircle() {
  ctx.beginPath();
  ctx.arc(SIZE / 2, SIZE / 2, r_out, 0, 2 * Math.PI);
  ctx.fillStyle = `rgba(${COLOR}, ${opa})`;
  ctx.fill();
}
//放大字体 大小随内圈一起变化
function drawText(val) {
  //font放大倍数和内圈一样
  ctx.font = (val / R_SMALL) * 60 + "px Arial";
  ctx.textBaseline = "center"; //设置字体底线居中
  ctx.textAlign = "center"; //设置字体对齐方式居中
  ctx.fillStyle = `rgba(255,255,255,1)`;
  ctx.fillText("✔", 70, 90);
}

清除画布

//清除画布
function clear() {
  ctx.clearRect(0, 0, SIZE, SIZE);
}

循环递归函数

//循环递归函数
(function start() {
  move.jump(); //开始执行
  requestAnimationFrame(() => {
    start();
  });
})()

script部分完整代码:

<script type="module">
  import MoveCrycle from "./js/MoveCrycle.js";

  let cavas = document.getElementById("canvas");
  var ctx = cavas.getContext("2d");

  const COLOR = "75, 167, 118";
  const R_SMALL = 45;//内圈初始大小
  const SIZE = 140; //画布宽高
  const R_OUT_STEP = 0.3 * 2.5; //外圈变化系数
  const OPA_STEP = 0.028;

  let r_out = R_SMALL; //外圈半径累加值
  let opa = 1; //外圈透明度累加值

  let move = new MoveCrycle({
    from: R_SMALL, //初始值
    to: R_SMALL * 1.35, //最大1.35倍
    step: 0.3, //步长
    dingNums: [R_SMALL * 1.1, R_SMALL * 1.35], //外圈开始出现的区间内圈的1.1倍到1.35倍
  });
  //订阅数值变化事件,每次变化获取值渲染
  move.on("onchange", (value, step) => {
    //绘制内圆
    clear(); //清空画布
    //绘制内圈
    drawInnerCircle(value);
    //绘制外圈
    drawOutCircle();
    //文字放在最后,不然会被上面的圆属性fillStyle覆盖
    drawText(value);
  });
  //再设定的区间触发事件
  move.on("onding", (value) => {
    r_out += R_OUT_STEP; //外圈变大
    opa -= OPA_STEP; //外圈变大的同时透明度减小
  });
  //动画圈开始变大
  move.on("onlarge", (value) => {
    opa = 1; //开始变大,透明度为1
    r_out = 45; //开始变大时半径回复到初始值
  });
  //动画圈开始变小
  move.on("onsmall", (value) => {
    //控制外圈光环-隐藏
    opa = 0;
  });

  //绘制内圆
  function drawInnerCircle(val) {
    ctx.beginPath();
    ctx.arc(SIZE / 2, SIZE / 2, val, 0, 2 * Math.PI);
    ctx.fillStyle = `rgba(${COLOR}, 1)`;
    ctx.fill();
  }
  //绘制外圈圆 变大的同时透明度下降
  function drawOutCircle() {
    ctx.beginPath();
    ctx.arc(SIZE / 2, SIZE / 2, r_out, 0, 2 * Math.PI);
    ctx.fillStyle = `rgba(${COLOR}, ${opa})`;
    ctx.fill();
  }
  //放大字体 大小随内圈一起变化
  function drawText(val) {
    //font放大倍数和内圈一样
    ctx.font = (val / R_SMALL) * 60 + "px Arial";
    ctx.textBaseline = "center"; //设置字体底线居中
    ctx.textAlign = "center"; //设置字体对齐方式居中
    ctx.fillStyle = `rgba(255,255,255,1)`;
    ctx.fillText("✔", 70, 90);
  }
  //自执行循环递归函数
  (function start() {
    move.jump(); //开始执行
    requestAnimationFrame(() => {
      start();
    });
  })()

  //清除画布
  function clear() {
    ctx.clearRect(0, 0, SIZE, SIZE);
  }
</script>

以上。
附全文源码地址:github源码demo在线预览地址
各位大佬,不要忘了给我点赞+评论+收藏
微信图片_20220306173458.jpg

  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
这个问题涉及到前后端的代码实现,需要分别给出前端和后端的代码示例。 前端实现: 1. 安装 pdf.js 库:可以使用 npm 安装,命令为 `npm install pdfjs-dist --save` 2. 在 Vue 组件中引入 pdf.js 库: ``` import pdfjsLib from 'pdfjs-dist' ``` 3. 在组件中定义预览 PDF 的方法,代码如下: ``` previewPdf() { // 从服务器获取 PDF 文件的 URL const url = 'http://localhost:8080/api/pdf/preview?fileId=1234567890' // 创建一个 canvas 用于绘制 PDF const canvas = this.$refs.previewCanvas // 获取 PDF 渲染上下文 const context = canvas.getContext('2d') // 加载 PDF 文件 pdfjsLib.getDocument(url).promise.then(pdf => { // 获取 PDF 第一页 pdf.getPage(1).then(page => { // 设置 canvas 尺寸与页面大小相同 const viewport = page.getViewport({scale: 1}) canvas.width = viewport.width canvas.height = viewport.height // 渲染 PDF 到 canvas 上 page.render({ canvasContext: context, viewport: viewport }) }) }) } ``` 4. 在模板中添加 canvas 和按钮,代码如下: ``` <template> <div> <canvas ref="previewCanvas"></canvas> <button @click="previewPdf">预览 PDF</button> </div> </template> ``` 后端实现: 1. 使用 Spring Boot 框架实现一个 RESTful API,用于获取 PDF 文件的 URL。代码示例如下: ``` @RestController @RequestMapping("/api/pdf") public class PdfController { @Autowired private SftpService sftpService; @GetMapping("/preview") public ResponseEntity<Resource> previewPdf(@RequestParam("fileId") String fileId) { // 从 SFTP 服务器上下载文件到本地临时目录 File tempFile = sftpService.download(fileId); // 将文件转换为 Spring Resource 对象 Path path = tempFile.toPath(); ByteArrayResource resource = null; try { resource = new ByteArrayResource(Files.readAllBytes(path)); } catch (IOException e) { e.printStackTrace(); } // 返回文件内容和响应头 HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=" + tempFile.getName()); headers.add(HttpHeaders.CONTENT_TYPE, "application/pdf"); headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(tempFile.length())); return ResponseEntity.ok() .headers(headers) .body(resource); } } ``` 2. 在 Spring Boot 应用程序中配置 SFTP 服务器连接信息和文件保存路径,代码示例如下: ``` @Configuration public class SftpConfig { @Value("${sftp.host}") private String host; @Value("${sftp.port}") private int port; @Value("${sftp.username}") private String username; @Value("${sftp.password}") private String password; @Value("${sftp.remoteDirectory}") private String remoteDirectory; @Value("${sftp.localDirectory}") private String localDirectory; @Bean public SftpService sftpService() { SftpConfig config = new SftpConfig(); config.setHost(host); config.setPort(port); config.setUsername(username); config.setPassword(password); config.setRemoteDirectory(remoteDirectory); config.setLocalDirectory(localDirectory); return new SftpServiceImpl(config); } } ``` 3. 实现 SftpService 接口,用于下载 PDF 文件到本地临时目录。代码示例如下: ``` public interface SftpService { File download(String fileId); } @Service public class SftpServiceImpl implements SftpService { private final SftpConfig config; public SftpServiceImpl(SftpConfig config) { this.config = config; } @Override public File download(String fileId) { Session session = null; ChannelSftp channel = null; try { JSch jsch = new JSch(); session = jsch.getSession(config.getUsername(), config.getHost(), config.getPort()); session.setPassword(config.getPassword()); session.setConfig("StrictHostKeyChecking", "no"); session.connect(); channel = (ChannelSftp) session.openChannel("sftp"); channel.connect(); channel.cd(config.getRemoteDirectory()); // 生成本地临时文件名 String localFileName = UUID.randomUUID().toString() + ".pdf"; File localFile = new File(config.getLocalDirectory() + File.separator + localFileName); // 下载文件到本地临时目录 channel.get(fileId, localFile.getAbsolutePath()); return localFile; } catch (JSchException | SftpException e) { e.printStackTrace(); } finally { if (channel != null) { channel.disconnect(); } if (session != null) { session.disconnect(); } } return null; } } ``` 需要注意的是,SFTP 服务器的连接信息需要在配置文件中进行配置,例如: ``` sftp.host=192.168.1.100 sftp.port=22 sftp.username=admin sftp.password=123456 sftp.remoteDirectory=/pdf sftp.localDirectory=/tmp ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值