Js 实现 marquee 效果

使用RequestAnimationFrame,核心部分就是利用transformX实现位移

Js 逻辑写的比较挫,还要想想怎么改进,或者有更好的思路。

marquee的要求是两段文字的间隔能人为的控制,所以用了两个重复的p标签。

然后就是如何计算第一个p标签和第二个p标签移出了可视区域,可视区域不一定是屏幕宽度,可能是一个div设置了widthoverflow实现的。

另外就是从左往右以及从右往左的区别。

从右往左

一开始p标签的位置是 position:absolute; left: 0,也就是在‘可视区域’的左边,从右往左就是移动负值。

如何判断移出‘可视区域’? 利用倍数来计算,

  1. 实际文字的宽度 / 可视区域的宽度得到3、3.5、4之类的一个倍数,用这个倍数和
  2. 目前正在变化时拿到的translateX的值 / 可视区域的宽度

假设是3倍,那么第二步计算出的值如果正好是3,说明文字的末尾已经出现在‘可视区域’,此时➕一个系数x,就可以实现两段文字的间隔(x按照实际想要的间隔自行设置)。

此时第一段文字+间隔 已经全部出现在可视区域了,接下来就要让第二段文字开始移动。第二段文字的起始位置就是‘可视区域’的宽度。

然后判断文字全部移出‘可视区域’判断 第二步骤的 倍数 - 第一步的倍数 < 一个允许范围的误差值即可。

从左往右

同样的思路,从左往右,刚开始第一个p标签也是位于 left: 0 的位置。

从左往右比较简单,translateX 移动位置 >= '可视区域宽度' 即为移出可视区域,然后将第一个p标签的 translateX 设置为 -自身宽度即成为了从左边出来的元素。

两个p标签间隔多少就看,第一个p标签开始移动多少距离后让第二个p标签开始移动,这个值就可以直接设定了。

 export default {
    data() {
      return {
        x1: 0,
        x2: 0,
        textWidth: 'auto',
        raf: '',
        timeout: '',
        currentDirection: true
      }
    },
    watch: {
      during: function () {
        this.marquee();
      },
      content: function () {
        this.marquee();
      },
      width: function () {
        this.marquee();
      },
      fontsize: function () {
        this.marquee();
      },
      direction: function (newVal) {
        switch (newVal) {
          case 'right-left':
            this.currentDirection = true;
            break;
          case 'left-right':
            this.currentDirection = false;
            break;
        }
        this.marquee();
      }
    },
    methods: {
      debounce: function (func, wait, immediate) {
        let timeout;
        return function () {
          let context = this, args = arguments;
          let later = function () {
            timeout = null;
            if (!immediate) func.apply(context, args);
          };
          let callNow = immediate && !timeout;
          clearTimeout(timeout);
          timeout = setTimeout(later, wait);
          if (callNow) func.apply(context, args);
        };
      },
      marquee: function () {
        this.debounce(() => {
          this.$nextTick(() => {
            this.animation();
          })
        }, 1000, true)()
      },
      animation: function () {
        window.cancelAnimationFrame(this.raf);
        this.x1 = 0;
        let wrap = this.width > this.$el.clientWidth ? this.$el.clientWidth : this.$refs.wrap.clientWidth;
        let textWidth = this.$refs.content.clientWidth;
        this.textWidth = textWidth + 'px';
        if (textWidth < wrap) {
          this.textWidth = wrap + 50 + 'px';
          textWidth = wrap + 50;
        }
        let originText2 = this.currentDirection ? wrap : (textWidth) * -1 - 20;
        this.x2 = originText2;
        let flag1 = true;
        let flag2 = false;
        let distance = textWidth / wrap;
        let current1 = 0;
        let current2 = 0;
        let test = () => {
          flag1 && (this.x1 = this.currentDirection ? this.x1 - this.during : this.x1 + this.during);
          flag2 && (this.x2 = this.currentDirection ? this.x2 - this.during : this.x2 + this.during);
          if (this.currentDirection) {
            current1 = Math.abs(this.x1 / wrap);
            current2 = Math.abs(this.x2 / wrap);
            Math.abs(current1 - distance + 0.9) <= 0.05 && (flag2 = true); //启动第二个
            Math.abs(current2 - distance + 0.9) <= 0.05 && (flag1 = true);//启动第1个
            if (Math.abs(current1 - distance) <= 0.02) { //第一个超出屏幕
              this.x1 = wrap; //回到起始位置
              flag1 = false;
            }
            if (Math.abs(current2 - distance) <= 0.02) { //第2个超出屏幕
              this.x2 = wrap;
              flag2 = false;
            }
          } else {
            (this.x1 > 0 && this.x1 <= this.during) && (flag2 = true);
            (this.x2 > 0 && this.x2 <= this.during) && (flag1 = true);
            if (this.x1 >= wrap) {
              this.x1 = originText2;
              flag1 = false;
            }
            if (this.x2 >= wrap) {
              this.x2 = originText2;
              flag2 = false
            }
          }

          this.raf = window.requestAnimationFrame(test);
        };
        this.raf = window.requestAnimationFrame(test)
      }
    },
    mounted() {
      this.marquee();
    }
  }
<div class="scrolling-text">
      <a class="scrolling-text-wrap" v-href="href" ref="wrap" :style="{
        'font-size':FONTSIZE+'rem',
        color:color,
        width:WIDTH+'rem',
        margin:TOP+'rem 0 0 '+LEFT+'rem'
        }">
        <p :style="{
         transform:'translateX(' + x1 + 'px)',
         'width': textWidth
              }">
          <span class="text-content" v-text="content" ref="content"></span>
        </p>
        <p :style="{
         transform:'translateX(' + x2 + 'px)',
         'width': textWidth
              }">
          <span class="text-content" v-text="content"></span>
        </p>
      </a>
 </div>
.scrolling-text {
      position: relative;
      width: 100%;
      height: 100%;
      text-align: center;
      overflow: hidden;
      font-size: 0;
      z-index: 5;
      .scrolling-text-wrap {
        position: relative;
        display: inline-block;
        white-space: nowrap;
        overflow: hidden;
        &:before {
          content: 'M';
          visibility: hidden;
        }
        p {
          position: absolute;
          left: 0;
          top: 0;
          display: inline-block;
          font-size: inherit;
          color: inherit;
          white-space: nowrap;
          animation-timing-function: linear;
          animation-iteration-count: infinite;
          animation-direction: initial;
          animation-fill-mode: backwards;
          animation-play-state: initial;
          span {
            display: inline-block;
          }
        }
      }
    }