用 JavaScript 实现手势库 — 手势动画应用【前端组件化】

前端《组件化系列》目录

通过上几篇文章,我们对动画已经有了更深入的了解。这里我们就回到我们之前写好的 Carousel 组件。

如果我们回头去看看我们之前写的 Carousel 代码,我们会发现这里的代码我们真的是写的太粗躁了。

我们的拖拽功能中的点击和拖动都是分不清楚的。然后我们也是没有做 flick 的这种判断。动画的时间也是用 CSS 动画来完成的。

更不用说,这个拖拽动画是否可以暂停、继续这样的逻辑。

那么这里我们就把我们上几篇在文章中实现的 Animation(动画库)和 Gesture(手势库)应用进来,从而实现一个完善的 Carousel(轮播组件)。

同学们注意: 只看这篇文章是无法看懂的,因为这篇文章的内容,都是基于之前的《轮播组件》、《实现动画库》、《实现手势库》三篇文章的内容。还没有学习这三篇文章的,请先前往学习再回来看这里哦~


Gesture 库应用

我们之前写的 Gesture, 是一个单独可用的手势库。但是如果它是作为一个正式的一个 Library,我们还是需要对它进行一些单元测试,或者一些代码 lint 风格的检查等等。

最后,它是可以单独作为一个 npm 模块去安装使用。不过我们目前的 Gesture 库还没有这些基础设施。所以我们就直接拿过来用了。

不过很多公司确实也是这样用的,写一个库,然后靠着业务去提升它的稳定性。这个也不能说是错误。在工作中也有很多库是通过 end to end 的测试来覆盖它的功能的。

从复杂性来讲,animation、gesture 和 carousel 都有一定的复杂性。当我们做了架构拆分之后,我们分别独立管理了这三部分的复杂性。否则如果这三部分融在一起的话,绝对会要了我们的命的。

讲那么多,我们一起正式进入改装我们原来的 carousel 组件吧。

重构 render()

首先我们需要注释掉原来 render() 里面的逻辑:

render() {
    this.root = document.createElement('div');
    this.root.classList.add('carousel');

    for (let picture of this.attributes.src) {
      let child = document.createElement('div');
      child.style.backgroundImage = `url('${picture}')`;
      this.root.appendChild(child);
    }
    
    let children = this.root.children;
    let position = 0;
    
    return this.root;
    
    /* ... 保留到这里 ... */
}

然后我们需要重新使用我们 gesture 库中的 enableGesture() 来启动所有手势操作的事件监听。

要使用这个函数,我们首先需要引入我们的 gesture 库:

import { enableGesture } from './gesture.js';

接着我们就可以在 this.root 上去 enableGesture,在 let position = 0,后面加入一下代码:

enableGesture(this.root);

/* 我们加入这个监听来看看我们的手势事件是否启动了 */
this.root.addEventListener("pan", event => {
	console.log(event.clientX);
})

运行起来之后,我们就发现在网页端和移动端的 pan 事件都是被正常监控了。

上图中,整数的就是网页端的输出,而的有较多小数点的就是移动端的输出。

虽然事件是正常监听了,但是我们拖拽的时候,图片并没有任何移动的。所以我们还重新加入图片移动的逻辑。这段逻辑就在我们之前 mousemove 的事件之中。

实现 pan 事件

那么我们就来重新实现一下这部分代码。

this.root.addEventListener("pan", event => {
	// 之前的 startX 我们可以直接在 event 中取得
    // 因为我们在实现 gesture 的时候就都挂载到 event 中了
    let x = event.clientX - event.startX;
    let current = position - (x - (x % 500)) / 500;
    
    /* 剩余这些代码都可以直接拿过来用就好了 */
    for (let offset of [-1, 0, 1]) {
      let pos = current + offset;
      // 计算图片所在 index
      pos = (pos + children.length) % children.length;

      children[pos].style.transition = 'none';
      children[pos].style.transform = `translateX(${-pos * 500 + offset * 500 + (x % 500)}px)`;
    }
})

这样我们就实现了 move(拖拽鼠标移动)这部分的逻辑了。我们先来看看是不是已经可以拖动了。

Wonderful,我们非常方便就实现了拖拽移动图片了。但是如果我们拖动到超出三张以上的图片的时候,console 中就会抛出一下错误:

这又是为什么呢?我们先来看看报错的位置。

首先是 “超出 3 张图片时”,第二是 children 中没有当前 pos 的元素。根据这两个线索,我们就知道是计算出来的 pos 是有问题的。噢噢!我们的 pos + children.length 其实是有可能会出现负数的,所以这里才会有问题。

所以我们要做的就是确保这个 pos 不会变成负数值,这里我们就可以运用数论里面的一个方法:

pos = (pos % children.length + children.length) % children.length;

让我们的 pos 先用 children.length 取余数,这样不管 pos 这个负数有多大,都会是一个小于 children.length 的一个值。

实现 panend 事件

这样我们就不会再报错了,接下来我们就可以继续取追加 “鼠标停下” 时的逻辑。鼠标停顿之前我们是用 “mouseup” 这个事件的。其实就是对应我们的 gesture 库中的 “panned” 事件。

除了这个事件名不同之外,其实其中的处理逻辑是一样的。只不过我们的 startX 就可以直接在我们的 event 中取就可以了。

this.root.addEventListener("panned", event => {
	// 之前的 startX 我们可以直接在 event 中取得
    // 因为我们在实现 gesture 的时候就都挂载到 event 中了
    let x = event.clientX - event.startX;
    position = position - Math.round(x / 500);

    for (let offset of [0, -Math.sign(Math.round(x / 500) - x + 250 * Math.sign(x))]) {
      let pos = position + offset;
      // 计算图片所在 index
      pos = ((pos % children.length) + children.length) % children.length;

      children[pos].style.transition = '';
      children[pos].style.transform = `translateX(${-pos * 500 + offset * 500}px)`;
    }
})

到这里我们完全使用 Gesture 库来替代了我们原来的手势事件逻辑了。显然我们这里的代码相比之前是要清晰的多。更加遵循架构设计中的 “单一应用” 原则。我们 Carousel 中只有关于轮播图相关的逻辑,而所有手势事件的逻辑都是内聚到单独的 Gesture 库中。


Timeline 库应用

接下来我们来把 Timeline 的时间线也应用到我们的 Carousel 组件当中,并且使用我们的 Animation 来替换原来的 CSS 动画。

在这之前,我们先要在 carousel.js 中先引入我们的 Timeline, Animation 库和 Ease 动画。

import { Timeline, Animation } from './animation.js';
import { Ease } from './ease.js';

重构自动轮播动画

首先我们来看看怎么用 Timeline 和 Animation 来重构我们的自动轮播代码。

这里我们需要注意的是,在 main.html 中的 CSS 部分,我们之前给 .carousel > div 这写元素赋予了 transition 属性。这个属性会与我们 Animation 的动画产生冲突,所以我们要先把这个属性给注释掉。

.carousel > div {
  width: 500px;
  height: 281px;
  background-size: contain;
  display: inline-block;
  /* transition: ease 0.5s; */
}

接下来我们看看原来的自动轮播代码,之前是用 currentIndex(当前图片的 index)来寻找目标图片的。其实可以用我们手势事件部分用到的 position 替代这个值的。所以我们可以把这部分替换过来。

setInterval 前面计算图片节点部分的代码是没有任何问题的,所以我们可以保留,不需要做任何修改:

let children = this.root.children;
// 下一张图片的 index
let nextIndex = (position + 1) % children.length;

// 当前图片的节点
let current = children[position];
// 下一张图片的节点
let next = children[nextIndex];

接下来的那一部分代码,我们之前是使用一个 16 毫秒的 setTimeout 来执行我们的动画逻辑的。这部分就可以使用我们的 Timeline 来代替。

首先我们在 enableGesture 之前实例化,并且启动一个 Timeline。

/* ...代码 */

let timeline = new Timeline;
timeline.start();

enableGesture(this.root);

/* ...代码 */

我们注意到之前在移动图片的时候都是用百分比作为单位的。但是为了更加精确的操纵我们的动画,我们需要把单位都换成像素(px)。

let next 之后,加入我们的轮播动画。首先是移动当前窗口内的图片:

  • 元素: current.style
  • 动画属性: ‘transform’
  • 开始属性值: -position * 500
  • 结束属性值: -500 * (position + 1)
  • 动画时长: 500
  • 延迟值: 0
  • 动画函数: ease
  • 动画模版: v => `translate(${v}px)`
// 先移动当前图片离开当前位置
timeline.add(new Animation(
    current.style,
    'transform',
    -position * 500, 
    -500 * (position + 1),
    0,
    ease,
    v => `translateX(${v}px)`
))

同时我们要移动下一张图的位置:

  • 元素: next.style
  • 动画属性: ‘transform’
  • 开始属性值: 500 - nextIndex * 500
  • 结束属性值: -500 * nextIndex
  • 动画时长: 500
  • 延迟值: 0
  • 动画函数: ease
  • 动画模版: v => `translate(${v}px)`
// 移动下一张图片到当前显示的位置
timeline.add(new Animation(
    next.style,
    'transform',
    500 - nextIndex * 500, 
    -500 * nextIndex,
    0,
    ease,
    v => `translateX(${v}px)`
))

最后,我们更新当前位置的 index:

// 最后更新当前位置的 index
position = nextIndex;

就这样,我们的自动轮播这部分重构完成了。

自动轮播和拖拽我们都重构完毕了,但是我们发现当我们在拖拽的时候,自动轮播会影响我们的拖拽。所以接下来我们就在解决轮播组件里最复杂的部分。


实现手势 + 轮播

首先,一旦我们鼠标或者手指点击我们轮播图的图片,我们就需要把图片自动轮播给停下。

这个就需要在我们 start 事件触发的时候去处理。

this.root.addEventListener('start', event => {
  timeline.pause();
});

这里我们直接调用 timeline.pause() 来停止我们的时间线。

等一下,好像不对呀,我们的手势库里面并没有 dispatch 一个 start 事件哦。是的,所以这里我们就回去 gesture.js 中添加这个 start 事件的触发。

在 gesture 库的 Recognizer(识别器)中找到 start 方法。在这个方法的靠前的位置添加我们的 start 事件触发。

start(point, context) {
    (context.startX = point.clientX), (context.startY = point.clientY);

    this.dispatcher.dispatch('start');
    
    /* 其余的代码... */
}

但是停止之后,我们再去拖拽的时候,就会发现我们图片位置就开始发生异常。

这个问题的是因为,我们的动画造成的图片位移的距离,并没有同步给我们手势计算。所以手势在计算位移的时候,认为当前图片还是第一张图片。

动画偏移距离

那么接下来我们要去算动画产生的位移距离,并且包含在我们手势计算中。

首先我们需要用一个变量,记录动画开始的时间:

render() {
    // 在 render 函数的这个位置
    // 加入一个 t 变量的声明

    /* ... 省略了上面的代码 ... */ 
    enableGesture(this.root);

    let children = this.root.children;
    let position = 0;
    let t = 0;

    /* ... 省略了下面的代码 ... */
}

然后在 setInterval 执行动画的地方记录动画开始时间:

render() {
    /* ... 省略了上面的代码 ... */ 
    
    setInterval() {
        let children = this.root.children;
        // 下一张图片的 index
        let nextIndex = (position + 1) % children.length;

        // 当前图片的节点
        let current = children[position];
        // 下一张图片的节点
        let next = children[nextIndex];
        
        // 记录动画开始时间
        let t = Date.now();
        
        /* ... */ 
    }
    
    /* ... 省略了下面的代码 ... */
}

在 start 时间触发的时候,也就是我们鼠标或者手指点击轮播图的时候,我们就计算当前动画播放的时间进度。这个进度我们可以根据当前动画已经播放了的时长来计算。

使用我们获得的时间进度,我们就可以通过 ease(progress) * 500 - 500 方法来获得当前动画进度所在的偏移距离。

这里的 ease(progress) 会返回一个 0 到 1 的值,根据二次贝塞尔曲线的概念,ease 会返回一个动画的进度(也就是动画执行了百分之多少)。使用这个进度乘于每个图片的 500 宽度。我们就可以得到一个当前图片的偏移距离。

这里因为我们已经播放到下一帧了,所以我们还需要再减到 500(一个图片的位置)。

let ax = 0; // 动画产生的 x 偏移距离

this.root.addEventListener('start', event => {
  timeline.pause();
  // 当前时间 - 动画开始时间 = 动画已经播放的时长
  // 播放时长 / 1500 = 时间进度
  let progress = (Date.now() - t) / 1500;
  ax = ease(progress) * 500 - 500;
});

注意: 我们这里为什么用 1500?因为我们要调试,所以我们把所有动画的时间都改为 1500 毫秒,这样更加缓慢的动画会更利于我们调试。所以所有的图片轮播的效果也一样,改为使用 1500 毫秒。最后,等我们所有的功能都完善之后,都改回 500 毫秒即可。

最后在计算图片所在的 x 位置的时候,减掉自动轮播动画产生的 x 偏移距离 ax,我们需要在 pan 和 panend 两个事件监听中修改这个逻辑:

let x = event.clientX - event.startX - ax;

这样我们在拖拽在时候,就不会再出现图片错位的现象的。但是如果我们拖拽的时候如果停下来不动的话,我们会发现图片还是在轮播。

这个问题是因为我们的自动播放动画的 setInterval 并没有停止,所以自动播放还是在跑的。所以我们还需要把这个 setInterval 给管理起来。

独立管理动画 Interval

要管理这个 setInterval 也很简单,只要把它赋予给一个变量,然后在 start 实现触发的时候 clear 掉这个 interval 即可。

首先我们把 setInterval 中的处理逻辑抽取出来单独储存:

// 动画处理逻辑
let nextAnimation = () => {
  let children = this.root.children;
  // 下一张图片的 index
  let nextIndex = (position + 1) % children.length;

  // 当前图片的节点
  let current = children[position];
  // 下一张图片的节点
  let next = children[nextIndex];

  t = Date.now();

  // 先移动当前图片离开当前位置
  timeline.add(
    new Animation(
      current.style,
      'transform',
      -position * 500,
      -500 * (position + 1),
      1500, // 动画改用了 1500 毫秒
      0,
      ease,
      v => `translateX(${v}px)`
    )
  );

  // 移动下一张图片到当前显示的位置
  timeline.add(
    new Animation(
      next.style,
      'transform',
      500 - nextIndex * 500,
      -500 * nextIndex,
      1500, // 动画改用了 1500 毫秒
      0,
      ease,
      v => `translateX(${v}px)`
    )
  );

  // 最后更新当前位置的 index
  position = nextIndex;
};

let ax 的声明下方加入 handler 变量声明,这个是用来储存我们的动画 interval 的:

let handler = null; // 动画 interval

把之前的 setInterval 赋予给 handler:

// 当前图片的 index
handler = setInterval(nextAnimation, 3000);

最后我们就可以在 start 触发时间里面 clear 掉这个 interval 了:

this.root.addEventListener('start', event => {
  timeline.pause();
  clearInterval(handler);
  // 获取动画的进度
  if (Date.now() - t < 1500) {
    let progress = (Date.now() - t) / 1500;
    ax = ease(progress) * 500 - 500; // 获取图片偏移的距离
  } else {
    // 如果没有完成一个 1500 毫秒的动画
    // x 的偏移就可以忽略了
    ax = 0;
  }
});

这样,我们把图片捡起来之后就不会再出现任何问题了。但是,对 “但是”,还有问题。当我们放开之后,图片又错位了。

这个是因为我们还没有处理鼠标松开事件的逻辑。接下来我们一起补充这一块逻辑漏洞。

这一块逻辑我们需要在 panend 中去实现,其实之前 panend 中的逻辑是不健全的(如果还记得的同学,我们之前是偷懒了的。我们直接把自动播放动画注释掉,然后分开来实现我们的手势拖拽轮播逻辑的。)

所以这里我们需要基于 pan 事件中的逻辑,重新来编写我们 panend 的逻辑。

重写 panend 事件

因为我们在鼠标或者手指点击图片的时候,就停止的 timeline 时间线。自然在我们鼠标或者手指松开的时候就要重启 timeline,这样才能让动画继续播放。

所以第一个逻辑就是重启 Timeline:

this.root.addEventListener('panend', event => {
  timeline.reset();
  timeline.start();
});

第二步,就是要处理 panend 触发时,图片的动画。其实 panend 的动画的起点时和 pan 的动画是一致的,但是动画结束的点就不一致了。

Panend 结束的时候我们是需要获得一个 500 的倍数的。因为最终我们必须是看到一张完整的图片,而不能有其他的图片在两边的边缘。

所以,我们需要根据当前 x 的偏移值,计算我们当前的图片应该是往左边,还是往右边移动,从而完成这个图片的轮播效果。如果这个图片没有往左或者右边移动超过 50% 自身的长度,那么我们就应该让这个图片回到中间的位置。

而这个移动方向我们就叫它作 direction,它的值只会是 -1(左移)、0(回中)、 1(右移) 这三个值。

首先我们复用 pan 中的 x 和 current 值的计算:

let x = event.clientX - event.startX - ax;
let current = position - (x - (x % 500)) / 500;

然后我们加入 direction 的计算:

let direction = Math.round((x % 500) / 500);

这个 direction 的计算,无非就是把 x 除以 500 的余数,这个数值必然是小于 500 的。然后再除以 500 获得这个数值占比 500 的百分之多少。这个数字必然是 -1 到 1 之间。最后我们再用四舍五入获得一个 -1、0、1 三种结果。

有了 direction 值,我们就可以给当前这个图片加上一个动画了。

  • 元素: children[pos].style
  • 动画属性: ‘transform’
  • 开始属性值: -pos * 500 + offset * 500 + (x % 500)
  • 结束属性值: -pos * 500 + offset * 500 + direction * 500
  • 动画时长: 1500
  • 延迟值: 0
  • 动画函数: ease
  • 动画模版: v => `translate(${v}px)`

加上了这个 Animation 之后,其实我们之前的问题是已经解决了。但是我们最后没有更新 position 的值。这样拖拽的时候回出现有图片跳动的现象。

最后我们来一起看一下怎么去更新 position 的值。

我们上面动画中,无非就是三种情况:

  • 当前 position(位置)往左移动了一个图片的位置(direction = -1)
  • 当前 position 往有移动了一个图片的位置(direction = 1)
  • 当前 position 没有移动(direction = 0)

那么如果要计算下一个 position,无非就是 current 的位置 - direction 即可。

这里还有注意的一个点,如果我们拖动超过 3 个图片的位置时,position 就有可能会变成负数,所以我们在此使用之前用我们之前用过的手法,把 position 强制转为正数。

最后我们再补充一个逻辑,在 timeline start 的时候,我们再次启动一次我们动画的 setInterval,这样当我们停止拖拽的时候,自动轮播就会再次启动。

最后我们的 panend 的代码如下:

this.root.addEventListener('panend', event => {
  timeline.reset();
  timeline.start();
  handler = setInterval(nextAnimation, 3000);

  let x = event.clientX - event.startX - ax;
  let current = position - (x - (x % 500)) / 500;
  let direction = Math.round((x % 500) / 500);

  for (let offset of [-1, 0, 1]) {
    let pos = current + offset;
    // 计算图片所在 index
    pos = ((pos % children.length) + children.length) % children.length;

    children[pos].style.transition = 'none';
    timeline.add(
      new Animation(
        children[pos].style,
        'transform',
        -pos * 500 + offset * 500 + (x % 500),
        -pos * 500 + offset * 500 + direction * 500,
        1500,
        0,
        ease,
        v => `translateX(${v}px)`
      )
    );
  }

  position = current - direction;
  position = ((position % children.length) + children.length) % children.length;
});

实现 Flick 手势轮播

到此为止,我们的轮播组件算是比较完善的了。基本所有功能都是可以使用了。接下来我们给这个轮播组件加上最后一个功能,也就是我们之前手势库中单独实现的 Flick 事件。

如果在我们拖拽的时候快速往左或者右滑动的时候,就会触发 Flick 事件,同时也可以让我们图片往对应的方向换图片。所以如果 Flick 的事情触发了之后,我们是需要去改变我们图片移动的方向,也就是覆盖我们之前定义的 direction 的值。

根据我们之前设计的手势库,flick 事件是会在 panend 的事件中触发的。

在 panend 事件监听中的 direction 计算之后,我们加入 flick 事件触发的判断:

if (event.isFlick) {
  console.log(event.velocity);
}

我们来打印一下触发到一个 flick 时候的 velocity(滑动速度)。

我们发现 velocity 既然是 undefined。这是怎么回事呢?

大家不用怕,这个完全是可控的翻车,全是演技。如果我们回去 gesture.js 中查看 panend 的 dispatch,我们就会发现在 panend 被分发出来的时候,我们根本没有把 velocity 给传出来。

所以我们需要在这里把 velocity 给加上。

其实我们这里还需要分发一个 end 事件,这个在本文章后面会使用到。这里就先补充上了。在 panend 的事件之后,我们加上 end 事件的分发:

this.dispatcher.dispatch('panend', {
  startX: context.startX,
  startY: context.startY,
  clientX: point.clientX,
  clientY: point.clientY,
  isVertical: context.isVertical,
  isFlick: context.isFlick,
  velocity: v,
});

this.dispatcher.dispatch('end', {
  startX: context.startX,
  startY: context.startY,
  clientX: point.clientX,
  clientY: point.clientY,
  isVertical: context.isVertical,
  isFlick: context.isFlick,
  velocity: v,
});

这里修改好,我们就可以正常获得我们的 velocity(滑动速度)值了。

那么我们就来把 flick 事件的逻辑给添加进去。

  • 这里我们判断如果 velocity 是小于 0 的话,证明我们是一个负数的速度,也就是说图片应该是往左边移动。这里我们就往上取整即可。(最后获得 direction = -1)
  • 如果 velocity 是大于 0 的话,证明是一个正数的速度,也就是图片需要往右移动,这种情况我们就往下取整即可。(最后获得 direction = 1)
if (event.isFlick) {
  if (event.velocity < 0) {
    direction = Math.ceil((x % 500) / 500);
  } else {
    direction = Math.floor((x % 500) / 500);
  }
}

这样我们就完成了 flick 手势的实现了。

调整动画时间

之前为了实现功能的时候利于调试,我们把所有动画的时间都改为了 1500 毫秒。现在我们的功能都实现完毕了。这里我们就可以把所有的动画时间都从 1500 毫秒改回 500 毫秒,让所有的动画更加自然一点。

最后给大家看看完整的 carousel.js 的代码:

import { Component } from './framework.js';
import { enableGesture } from './gesture.js';
import { Timeline, Animation } from './animation.js';
import { ease } from './ease.js';

export class Carousel extends Component {
  // 构造函数
  // 创建 DOM 节点
  constructor() {
    super();
    this.attributes = Object.create(null);
  }
  setAttribute(name, value) {
    this.attributes[name] = value;
  }
  render() {
    this.root = document.createElement('div');
    this.root.classList.add('carousel');

    for (let picture of this.attributes.src) {
      let child = document.createElement('div');
      child.style.backgroundImage = `url('${picture}')`;
      this.root.appendChild(child);
    }

    let timeline = new Timeline();
    timeline.start();

    enableGesture(this.root);

    let children = this.root.children;
    let position = 0;
    let t = 0; // 动画开始时间
    let ax = 0; // 动画产生的 x 偏移距离
    let handler = null; // 动画 interval

    this.root.addEventListener('start', event => {
      timeline.pause();
      clearInterval(handler);
      // 获取动画的进度
      if (Date.now() - t < 500) {
        let progress = (Date.now() - t) / 500;
        ax = ease(progress) * 500 - 500; // 获取图片偏移的距离
      } else {
        ax = 0;
      }
    });

    this.root.addEventListener('pan', event => {
      // 之前的 startX 我们可以直接在 event 中取得
      // 因为我们在实现 gesture 的时候就都挂载到 event 中了
      let x = event.clientX - event.startX - ax;
      let current = position - (x - (x % 500)) / 500;

      for (let offset of [-1, 0, 1]) {
        let pos = current + offset;
        // 计算图片所在 index
        pos = ((pos % children.length) + children.length) % children.length;

        children[pos].style.transition = 'none';
        children[pos].style.transform = `translateX(${-pos * 500 + offset * 500 + (x % 500)}px)`;
      }
    });

    this.root.addEventListener('panend', event => {
      timeline.reset();
      timeline.start();
      handler = setInterval(nextAnimation, 3000);

      let x = event.clientX - event.startX - ax;
      let current = position - (x - (x % 500)) / 500;
      let direction = Math.round((x % 500) / 500);

      if (event.isFlick) {
        if (event.velocity < 0) {
          direction = Math.ceil((x % 500) / 500);
        } else {
          direction = Math.floor((x % 500) / 500);
        }
      }

      for (let offset of [-1, 0, 1]) {
        let pos = current + offset;
        // 计算图片所在 index
        pos = ((pos % children.length) + children.length) % children.length;

        children[pos].style.transition = 'none';
        timeline.add(
          new Animation(
            children[pos].style,
            'transform',
            -pos * 500 + offset * 500 + (x % 500),
            -pos * 500 + offset * 500 + direction * 500,
            500,
            0,
            ease,
            v => `translateX(${v}px)`
          )
        );
      }

      position = current - direction;
      position = ((position % children.length) + children.length) % children.length;
    });

    let nextAnimation = () => {
      let children = this.root.children;
      // 下一张图片的 index
      let nextIndex = (position + 1) % children.length;

      // 当前图片的节点
      let current = children[position];
      // 下一张图片的节点
      let next = children[nextIndex];

      t = Date.now();

      // 先移动当前图片离开当前位置
      timeline.add(
        new Animation(
          current.style,
          'transform',
          -position * 500,
          -500 * (position + 1),
          500,
          0,
          ease,
          v => `translateX(${v}px)`
        )
      );

      // 移动下一张图片到当前显示的位置
      timeline.add(
        new Animation(
          next.style,
          'transform',
          500 - nextIndex * 500,
          -500 * nextIndex,
          500,
          0,
          ease,
          v => `translateX(${v}px)`
        )
      );

      // 最后更新当前位置的 index
      position = nextIndex;
    };

    // 当前图片的 index
    handler = setInterval(nextAnimation, 3000);

    return this.root;
  }
  mountTo(parent) {
    parent.appendChild(this.render());
  }
}

我是来自《技术银河》的三钻,一位正在重塑知识的技术人。下期再见。


⭐️ 三哥推荐

开源项目推荐

Hexo Theme Aurora


在最近在版本 1.5.0 更新了以下功能:

预览

✨ 新增

  • 自适应 “推荐文章” 布局 (增加了一个新的 “置顶文章布局” !!)
    • 能够在“推荐文章”和“置顶文章”模式之间自由切换
    • 如果总文章少于 3 篇,将自动切换到“置顶文章”模式
    • 在文章卡上添加了“置顶”和“推荐”标签
    • 📖 文档
  • 增加了与 VuePress 一样的自定义容器 #77
    • Info 容器
    • Warning 容器
    • Danger 容器
    • Detail 容器
    • 预览
  • 支持了更多的 SEO meta 数据 #76
    • 添加了 description
    • 添加了 keywords
    • 添加了 author
    • 📖 文档

最近博主在全面投入开发一个可以 “迈向未来的” Hexo 主题,以极光为主题的博客主题。

如果你是一个开发者,做一个个人博客也是你简历上的一个亮光点。而如果你有一个超级炫酷的博客,那就更加是亮上加亮了,简直就闪闪发光。

如果喜欢这个主题,可以在 Github 上给我点个 🌟 让彼此都发光吧~

主题 Github 地址:https://github.com/auroral-ui/hexo-theme-aurora
主题使用文档:https://aurora.tridiamond.tech/zh/


VSCode Aurora Future


对,博主还做了一个 Aurora 的 VSCode 主题。用了Hexo Theme Aurora 相对应的颜色色系。这个主题的重点特性的就只用了 3 个颜色,减少在写代码的时候被多色多彩的颜色所转移了你的注意力,让你更集中在写代码之中。

喜欢的大家可以支持一下哦! 直接在 VSCode 的插件搜索中输入 “Aurora Future” 即可找到这个主题哦!~

主题 Github 地址:https://github.com/auroral-ui/aurora-future-vscode-theme
主题插件地址:https://marketplace.visualstudio.com/items?itemName=auroral-ui.aurora-future


Firefox Aurora Future

我不知道大家,但是最近我在用火狐浏览器来做开发了。个人觉得火狐还真的是不错的。推荐大家尝试一下。

当然我这里想给大家介绍的是我在火狐也做了一个 Aurora 主题。对的!用的是同一套的颜色体系。喜欢的小伙伴可以试一下哦!

主题地址:https://addons.mozilla.org/en-US/firefox/addon/aurora-future/

  • 10
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三钻

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值