滑动轮播

-- 全屏幕滑动轮播图

课程概要

本课程主要讲解 JavaScript 轮播图的原理和开发,在主站首页及营销页中,都会涉及到轮播图的使用。概括了元素的拷贝、回调函数、动画的知识点,还将讲解定时器与清除定时器的方法实现自动轮播。通过本课程,完成一个全屏幕滑动轮播图应用,大家将对轮播图的原理和开发有更深入的了解。

主要包含以下功能:

  • 点击下一个箭头,滑动到下一张卡片图
  • 点击上一个箭头,滑动到上一张卡片图
  • 点击对应的小圆点,滑动到对应的位置
  • 当屏幕容器发生变化时候,重新计算不影响显示效果

实现原理

克隆项目的第一个和最后一个分别放在最后一项和第一项,保证在第一项时点击上一个会平滑的展示最后一项的内容。同理在最后一项时候点击下一项会平滑展示第一项的内容。在滑动结束后,瞬间定位到实际位置,例如从第一张图往上滑动到展示克隆的最后一张图,然后定位到实际的最后一张图的实际位置上。

思路:

  1. 首先必须的及需要存储的值有 每个项目的宽度,有多少个项目(len),当前显示哪个项目
  2. 头尾添加各一个项目后,重设偏移距离
  3. 当前显示哪个项目的 index( n ) 值和偏移量 translateX 的关系 -( n + 1 )* x
  4. 点击获取当前的 index ,然后计算出新的 index ,再交给滚动事件去处理。
  5. 当页面宽度发生变化时候需要重新计算项目宽度和偏移量,因此需要监听浏览器的尺寸变化
  6. 当点击的 index 为 -1 时,及会滚动到我们克隆的项目中,在滚动完毕之后需要重新设置偏移距离,瞬间定位到第 len 个项目中去。
  7. 同理当点击的 index 为 原本项目的数量值时,会滚动到我们克隆的项目中,在滚动完毕之后需要重新设置偏移距离,瞬间定位到初始项目中去。
  8. 锁,如果连续点击的话,会不断的触发事件,因此我们要加把锁,在开始滚动时把锁关闭,等滚动完毕之后把锁打开。

知识点

本课程涉及到的主要知识点有:

  1. cloneNode 拷贝元素
<p id="para">text demo</p>
let p = document.getElementById("para");
let p_prime = p.cloneNode(true);
  1. prepend 从开始位置插入元素
```js
let parentElem = document.createElement('div');
let childElem = document.createElement('p');
parentElem.prepend(childElem);
// 把 p 元素添加到 div 元素的开始位置
  1. liner 匀速运动
/* 
* time     当前时间
* begin    开始位移
* end      结束位移
* duration 全程时间
* return   当前位移
*/
function linear(time, begin, end, duration) { 
  // 公式:当前时间/全程时间 = 发生位移/全程位置
  // 发生位移 = 全程位置 * 当前时间 / 全程时间
  // 当前位移 = 发生位移 + 开始位移
  return ( end - begin ) * time / duration + begin;
}
  1. requestAnimationFrame 针频动画

浏览器在下一次重绘之前调用指定的函数来更新动画

function startCircle(el){
  let startTime = Date.now(); // 获取启动时间
  let cycle = 4000; // 多少秒转一周
  requestAnimationFrame(function update(){
    let currentTime = Date.now(); // 获取当前时间
    let p = (currentTime - startTime) / cycle; // 计算转动多少周
    el.style.transform = `rotate(${360 * p }deg)` // 转化为角度
    requestAnimationFrame(update) // 下一次渲染继续执行
  })
}
var ball = document.getElementById('ball');
startCircle(ball);
function animateTo(begin,end,duration,changeCallback,finishCallback){
  // 动画开始的时间
  let startTime = Date.now();
  requestAnimationFrame(function update(){
    // 当前执行的时间
    let dataNow = Date.now();
    // 当前所消耗的时间
    let time = dataNow - startTime;
    // 当前发生的位移
    let value = PAGE.linear(time,begin,end,duration);
    // 执行变化事件回调
    typeof changeCallback === 'function' && changeCallback(value)
    // 如果动画结束时间大于当前执行时间
    if(startTime + duration > dataNow){
      // 在执行一次动画渲染
      requestAnimationFrame(update)
    }else{
      // 执行结束回调
      typeof finishCallback === 'function' && finishCallback(end)
    }
  })
}

实现步骤

  1. 创建单例对象
  2. 克隆项目
  3. 滑动到显示位置
  4. 绑定上一项滑动事件
  5. 绑定下一项滑动事件
  6. 加锁与解锁
  7. 标签切换与高亮
  8. 位置重置
  9. 监听浏览器窗口变化

创建单例对象

  1. 创建单例对象 PAGE
  2. 定义 PAGE.data 需要存放的数据,
  • index 当前第几个
  • duration 滑动时长
  • isLock 锁
  • translateX 偏移量
  • defaultLenght 默认的项目数量
  • itemWidth 单个项目的长度
  1. 创建 init 、bind 方法,并在 init 中调用 bind
  2. 调用 PAGE.init
// 1. 创建单例对象 PAGE
const PAGE = {
  // 2. 定义 PAGE.data 需要存放的数据
  data: {
    index: 0,
    duration: 500,
    isLock: false,
    translateX: 0,
    defaultLenght: null,
    itemWidth: null
  },
  // 3. 创建 init 、bind 方法,并在 init 中调用 bind
  init: function() {
    this.bind();
  },
  bind: function() {

  }
}

// 4. 调用 PAGE.init
PAGE.init();

克隆项目

  1. 创建 PAGE.clone 克隆项目方法
  2. 在 init 中调用 clone
  3. 获取 swiper-item 元素,并克隆第一项和最后一项
  4. 更新 PAGE.data 中 defaultLenght 、itemWidth、translateX 的值。
  5. 把克隆的第一项放到 swiper-list 的最后,把克隆的最后一项放在 swiper-list 的第一位
const PAGE = {
  ...,
  init: function() {
    // 2. 在 init 中调用 clone
    this.clone();
    this.bind();
  },
  ...,
  // 1. 创建 PAGE.clone 克隆项目方法
  clone: function() {
    // 3. 获取 swiper-item 元素,并克隆第一项和最后一项
    let swiperItem = document.getElementsByClassName('swiper-item');
    let firstItem = swiperItem[0].cloneNode();
    let lastItem = swiperItem[ swiperItem.length - 1].cloneNode();

    // 4. 更新 PAGE.data 中 defaultLenght 、itemWidth、translateX 的值。
    let swiperList = document.getElementById('swiper-list');
    let index = PAGE.data.index;
    let swiperItemWidth = swiperList.offsetWidth;
    PAGE.data.defaultLenght = swiperItem.length;
    PAGE.data.itemWidth = swiperItemWidth;
    PAGE.data.translateX = - (swiperItemWidth + swiperItemWidth * index);

    // 5. 把克隆的第一项放到 swiper-list 的最后,把克隆的最后一项放在 swiper-list 的第一位
    swiperList.appendChild(firstItem);
    swiperList.prepend(lastItem);
  },
}

PAGE.init();

滑动到显示位置

  1. 创建 Page.animateTo 动画函数和 linear 匀速函数
  2. 创建滑动函数 Page.goIndex,接收一个参数,为滑动到第几项
  3. 在 PAGE.data 中获取动画时间长、项目宽度、偏移量( duration、itemWidth、translateX )
  4. 根据传入的 index 计算出滑动结束的目标位置
  5. 调用 PAGE.animateTo 动画
  6. 在滑动过程回调中,设置 swiper-list 的偏移值
  7. 在滑动完毕的回调中,设置借结束位置及更新 PAGE.data 中的当前显示索引以及当前偏移量 ( index、translateX )
  8. 在 PAGE.clone 方法完毕后调用 Page.goIndex 并传入 PAGE.data.index 默认索引
const PAGE = {
  ...,
  clone: function() {
    ...,
    // 8. 在 PAGE.clone 方法完毕后调用 Page.goIndex 并传入 PAGE.data.index 默认索引
    PAGE.goIndex(index);
  },
  // 2. 创建滑动函数 Page.goIndex,接收一个参数,为滑动到第几项
  goIndex: function(index){
    // 3. 在 PAGE.data 中获取动画时间长、项目宽度、偏移量( duration、itemWidth、translateX )
    let swiperDuration = PAGE.data.duration;
    let swiperItemWidth = PAGE.data.itemWidth;
    let beginTranslateX = PAGE.data.translateX;
    // 4. 根据传入的 index 计算出滑动结束的目标位置
    let endTranslateX = - (swiperItemWidth + swiperItemWidth * index);

    // 5. 调用 PAGE.animateTo 动画,在滑动过程渲染回调中设置 swiper-list 的偏移值
    let swiperList = document.getElementById('swiper-list');
    PAGE.animateTo(beginTranslateX,endTranslateX,swiperDuration,function(value){
      // 6. 在滑动过程回调中,设置 swiper-list 的偏移值
      swiperList.style.transform = `translateX(${value}px)`;
    },function(value){
      // 7. 在滑动完毕的回调中,设置借结束位置及更新 PAGE.data 中的当前显示索引以及当前偏移量
      swiperList.style.transform = `translateX(${value}px)`;
      PAGE.data.index = index;
      PAGE.data.translateX = value;
    })
  },
  // 1. 创建 Page.animateTo 动画函数和 linear 匀速函数
  animateTo:function(begin,end,duration,changeCallback,finishCallback){
    let startTime = Date.now();
    requestAnimationFrame(function update(){
      let dataNow = Date.now();
      let time = dataNow - startTime;
      let value = PAGE.linear(time,begin,end,duration);
      typeof changeCallback === 'function' && changeCallback(value)
      if(startTime + duration > dataNow){
        requestAnimationFrame(update)
      }else{
        typeof finishCallback === 'function' && finishCallback(end)
      }
    })
  },
  linear: function(time, begin, end, duration) { 
    return ( end - begin ) * time / duration + begin;
  }
}

PAGE.init();

绑定上一项滑动事件

  1. 获取上一项元素 swiper-prev 并绑定 swpierPrev 在点击时触发
  2. 创建 swpierPrev 事件
  3. 获取 PAGE.data 中的当前索引值 index
  4. 调用 PAGE.goIndex 并传入 index - 1
const PAGE = {
  ...,
  bind: function() {
    // 1. 获取上一项元素 swiper-prev 并绑定 swpierPrev 在点击时触发
    let swiperPrev = document.getElementById('swiper-prev');
    swiperPrev.addEventListener('click',this.swiperPrev);
  },
  ...,
  // 2. 创建 swpierPrev 事件
  swiperPrev: function() {
    // 3. 获取 PAGE.data 中的当前索引值 index
    let index = PAGE.data.index;
    // 4. 调用 PAGE.goIndex 并传入 index - 1
    PAGE.goIndex(index - 1);
  },
  ...,
}

绑定下一项滑动事件

和绑定上一项滑动事件原理及流程一直,在调用 goIndex 的时候,传入下一个索引即可

  1. 获取上一项元素 swiper-next 并绑定 swpierNext 在点击时触发
  2. 创建 swpierNext 事件
  3. 获取 PAGE.data 中的当前索引值 index
  4. 调用 PAGE.goIndex 并传入 index + 1
const PAGE = {
  ...,
  bind: function() {
    ...,
    // 1. 获取下一项元素 swiper-next 并绑定 swpierNext 在点击时触发
    let swpierNext = document.getElementById('swiper-next');
    swpierNext.addEventListener('click',this.swpierNext);
  },
  ...,
  // 2. 创建 swpierNext 事件
  swpierNext: function() {
    // 3. 获取 PAGE.data 中的当前索引值 index
    let index = PAGE.data.index;
    // 4. 调用 PAGE.goIndex 并传入 index + 1
    PAGE.goIndex(index + 1);
  },
  ...,
}

加锁与解锁

我们发现在不断点击的时候,会出现 BUG,原因是在滑动的过程中点击,重新触发事件,在交替重复执行。

  1. 在 goIndex 事件中,调用动画之前获取 PAGE.data.isLock 判断
  2. 如果已经锁了,就直接返回,不进行滑动事件的触发
  3. 如果没锁,把锁锁上
  4. 在滑动动画完成的回调中,解锁
const PAGE = {
  ...,
  goIndex: function(index){
    let swiperDuration = PAGE.data.duration;
    let swiperItemWidth = PAGE.data.itemWidth;
    let beginTranslateX = PAGE.data.translateX;
    let endTranslateX = - (swiperItemWidth + swiperItemWidth * index);
    let swiperList = document.getElementById('swiper-list');
    
    // 1. 在 goIndex 事件中,调用动画之前获取 PAGE.data.isLock 判断
    let isLock = PAGE.data.isLock;
    // 2. 如果已经锁了,就直接返回,不进行滑动事件的触发
    if(isLock){
      return
    }
    // 3. 如果没锁,把锁锁上
    PAGE.data.isLock = true;

    PAGE.animateTo(beginTranslateX,endTranslateX,swiperDuration,function(value){
      swiperList.style.transform = `translateX(${value}px)`;
    },function(value){
      swiperList.style.transform = `translateX(${value}px)`;
      PAGE.data.index = index;
      PAGE.data.translateX = value;
      // 4. 在滑动动画完成的回调中,解锁
      PAGE.data.isLock = false;
    })
  },
  ...,
}

标签切换与高亮

  1. 获取所有 swiper-pagination-switch 元素
  2. 循环为其添加 data-index 属性并绑定 swiperSwitch 事件
  3. 新建 swiperSwitch 方法
  4. 获取当前点击元素的 dataset 中 index 的值,为对应的索引
  5. 调用 goIndex 并传入 index
  6. 新建 hightlight 标签高亮事件,接收第几项高亮的索引 index
  7. 去除所有 swiper-pagination-switch 的高亮
  8. 为索引下的 swiper-pagination-switch 添加高亮
  9. 在 goIndex 滑动结束的回调中调用
const PAGE = {
  ...,
  bind: function() {
    ...,
    // 1. 获取所有 swiper-pagination-switch 元素
    let swiperSwitch = document.getElementsByClassName('swiper-pagination-switch');
    // 2. 循环为其添加 data-index 属性并绑定 swiperSwitch 事件
    for (let i = 0; i < swiperSwitch.length; i++) {
      swiperSwitch[i].setAttribute('data-index', i);
      swiperSwitch[i].addEventListener('click',this.swiperSwitch);
    }
  },
  ...,
  // 3. 新建 swiperSwitch 方法
  swiperSwitch: function(e) {
    // 4. 获取当前点击元素的 dataset 中 index 的值,为对应的索引
    let index = e.target.dataset.index;
    // 5. 调用 goIndex 并传入 index
    index = Number(index);
    PAGE.goIndex(index);
  },
  // 6. 新建 hightlight 标签高亮事件,接收第几项高亮的索引 index
  hightlight: function(index) {
    // 7. 去除所有 swiper-pagination-switch 的高亮
    let swiperItem = document.getElementsByClassName('swiper-pagination-switch');
    for (let i = 0; i < swiperItem.length; i++) {
      swiperItem[i].className = 'swiper-pagination-switch';
    }
    // 8. 为索引下的 swiper-pagination-switch 添加高亮
    swiperItem[index].className = 'swiper-pagination-switch active';
  },
  goIndex: function(index) {
    ...
    PAGE.animateTo(beginTranslateX,endTranslateX,swiperDuration,function(value){
      ...
    },function(value){
      ...

      // 9. 在 goIndex 滑动结束的回调中调用
      PAGE.hightlight(index);
    })
  },
}

位置重置

在 goIndex 方法中判断,在什么时候,瞬间重置到哪个位置。

  1. 在动画回调完成时候,获取默认的项目长度
  2. 如果当前索引为 -1 时,也是就滑动到我们 clone 的最后一项
  3. 重设索引为最后一项,并重新计算偏移量
  4. 如果当前索引为项目长度,也就是滑动到我们 clone 的第一项
  5. 重设索引为第 0 项目,并重新计算偏移量
const PAGE = {
  ...,
  goIndex: function(index){
    let swiperDuration = PAGE.data.duration;
    let swiperItemWidth = PAGE.data.itemWidth;
    let beginTranslateX = PAGE.data.translateX;
    let endTranslateX = - (swiperItemWidth + swiperItemWidth * index);
    let swiperList = document.getElementById('swiper-list');

    let isLock = PAGE.data.isLock;
    if(isLock){
      return
    }

    PAGE.data.isLock = true;

    PAGE.animateTo(beginTranslateX,endTranslateX,swiperDuration,function(value){
      swiperList.style.transform = `translateX(${value}px)`;
    },function(value){
      // 1. 在动画回调完成时候,获取默认的项目长度
      let swiperLength = PAGE.data.defaultLenght;
      // 2. 如果当前索引为 -1 时,也是就滑动到我们 clone 的最后一项
      if(index === -1){
        // 3. 重设索引为最后一项,并重新计算偏移量
        index = swiperLength - 1;
        value =  - (swiperItemWidth + swiperItemWidth * index);
      }
      // 4. 如果当前索引为项目长度,也就是滑动到我们 clone 的第一项
      if(index === swiperLength){
        // 5. 重设索引为第 0 项目,并重新计算偏移量
        index = 0;
        value =  - (swiperItemWidth + swiperItemWidth * index);
      }

      swiperList.style.transform = `translateX(${value}px)`;
      PAGE.data.index = index;
      PAGE.data.translateX = value;
      PAGE.data.isLock = false;
      PAGE.hightlight(index);
    })
  },
  ...,
}

监听浏览器窗口变化

  1. 在 bind 中绑定浏览器 resize 变化事件 swiperReset
  2. 定义 swiperReset 方法
  3. 获取 swiper-list 变化后的宽度和当前所在索引
  4. 计算变化后的偏移量
  5. 重设 PAGE.data 中的偏移量和宽度
  6. 设置页面的偏移量
const PAGE = {
  ...,
  bind: function() {
    ...,
    // 1. 在 bind 中绑定浏览器 resize 变化事件 swiperReset
    window.addEventListener('resize',this.swiperReset)
  },
  // 2. 定义 swiperReset 方法
  swiperReset: function() {
    // 3. 获取 swiper-list 变化后的宽度和当前所在索引
    let swiperList = document.getElementById('swiper-list');
    let swiperItemWidth = swiperList.offsetWidth;
    let index = PAGE.data.index;
    // 4. 计算变化后的偏移量
    let translateX = - (swiperItemWidth + swiperItemWidth * index);
    // 5. 重设 PAGE.data 中的偏移量和宽度
    PAGE.data.itemWidth = swiperItemWidth;
    PAGE.data.translateX = translateX;
    // 6. 设置页面的偏移量
    swiperList.style.transform = `translateX(${translateX}px)`;
  }
}

代码示例

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>FullSwiper</title>
  <style type="text/css">
  *{ 
      margin: 0; 
      padding: 0;
  }
  .swiper-wrapper{
      position: relative;
      height: 360px;
      overflow: hidden;
  }

  .swiper-list{
      position: relative;
      display: flex;
      height: 100%;
  }
  .swiper-item{
      width: 100%;
      flex: none;
  }

  .swiper-pagination{
      position: absolute;
      bottom: 20px;
      left: 50%;
      transform: translateX(-50%);
  }
  .swiper-pagination-switch{
      display: inline-block;
      width: 20px;
      height: 20px;
      border-radius: 50%;
      background-color: #F2EFE6;
  }
  .swiper-pagination-switch.active{
      background-color: #E03636;
  }
  .swiper-arrow{
      position: absolute;
      width: 50px;
      height: 80px;
      top: 50%;
      transform: translateY(-50%);
      background-color: #EDD0BE;
      line-height: 80px;
      text-align: center;
      color: #666;
      cursor: pointer;
      opacity: 0;
      transition: all .2s ease;
  }
  .swiper-arrow-left{
      left: 100px;
  }
  .swiper-arrow-right{
      right: 100px;
  }
  .swiper-wrapper:hover .swiper-arrow{
      opacity: 1;
  }
  </style>
</head>
<body>
  <div class="swiper-wrapper" id="swiper-wrapper">
    <div class="swiper-list" id="swiper-list">
      <div class="swiper-item" style="background-color: #82A6F5;">1</div>
      <div class="swiper-item" style="background-color: #EAF048;">2</div>
      <div class="swiper-item" style="background-color: #9FF048;">3</div>
      <div class="swiper-item" style="background-color: #2A5200;">4</div>
      <div class="swiper-item" style="background-color: #F6D6FF;">5</div>
    </div>
    <div class="swiper-pagination">
      <span class="swiper-pagination-switch active"></span>
      <span class="swiper-pagination-switch"></span>
      <span class="swiper-pagination-switch"></span>
      <span class="swiper-pagination-switch"></span>
      <span class="swiper-pagination-switch"></span>
    </div>
    <a class="swiper-arrow swiper-arrow-left" id="swiper-prev">&lt;</a>
    <a class="swiper-arrow swiper-arrow-right" id="swiper-next">&gt;</a>
  </div>

  <script type="text/javascript">

    const PAGE = {
      data: {
        index: 0,
        duration: 500,
        isLock: false,
        translateX: 0,
        defaultLenght: null,
        itemWidth: null
      },
      init: function() {
        this.clone();
        this.bind();
      },
      bind: function() {
        let swiperPrev = document.getElementById('swiper-prev');
        swiperPrev.addEventListener('click',this.swiperPrev);
        let swiperNext = document.getElementById('swiper-next');
        swiperNext.addEventListener('click',this.swiperNext);

        let swiperSwitch = document.getElementsByClassName('swiper-pagination-switch');
        for (let i = 0; i < swiperSwitch.length; i++) {
          swiperSwitch[i].setAttribute('data-index', i);
          swiperSwitch[i].addEventListener('click',this.swiperSwitch);
        }

        window.addEventListener('resize',this.swiperReset)
      },
      swiperReset: function(e) {
        let swiperList = document.getElementById('swiper-list');
        let swiperItemWidth = swiperList.offsetWidth;
        let index = PAGE.data.index;
        let translateX = - (swiperItemWidth + swiperItemWidth * index);
        PAGE.data.itemWidth = swiperItemWidth;
        PAGE.data.translateX = translateX;
        swiperList.style.transform = `translateX(${translateX}px)`;
      },
      clone: function() {
        let swiperItem = document.getElementsByClassName('swiper-item');
        let firstItem = swiperItem[0].cloneNode();
        let lastItem = swiperItem[ swiperItem.length - 1].cloneNode();
        let swiperList = document.getElementById('swiper-list');
        let index = PAGE.data.index;
        let swiperItemWidth = swiperList.offsetWidth;
        PAGE.data.defaultLenght = swiperItem.length;
        PAGE.data.itemWidth = swiperItemWidth;
        PAGE.data.translateX = - (swiperItemWidth + swiperItemWidth * index);
        swiperList.appendChild(firstItem);
        swiperList.prepend(lastItem);
        PAGE.goIndex(index);
      },
      swiperPrev: function() {
        let index = PAGE.data.index;
        PAGE.goIndex(index - 1);
      },
      swiperNext: function() {
        let index = PAGE.data.index;
        PAGE.goIndex(index + 1);
      },
      swiperSwitch: function(e) {
        let index = e.target.dataset.index;
        index = Number(index);
        PAGE.goIndex(index);
      },
      goIndex: function(index){
        let swiperDuration = PAGE.data.duration;
        let swiperItemWidth = PAGE.data.itemWidth;
        let beginTranslateX = PAGE.data.translateX;
        let endTranslateX = - (swiperItemWidth + swiperItemWidth * index);
        let swiperList = document.getElementById('swiper-list');

        let isLock = PAGE.data.isLock;
        if(isLock){
          return
        }

        PAGE.data.isLock = true;
        PAGE.animateTo(beginTranslateX,endTranslateX,swiperDuration,function(value){
          swiperList.style.transform = `translateX(${value}px)`;
        },function(value){
          let swiperLength = PAGE.data.defaultLenght;
          if(index === -1){
            index = swiperLength - 1;
            value =  - (swiperItemWidth + swiperItemWidth * index);
          }
          if(index === swiperLength){
            index = 0;
            value =  - (swiperItemWidth + swiperItemWidth * index);
          }

          swiperList.style.transform = `translateX(${value}px)`;
          PAGE.data.index = index;
          PAGE.data.translateX = value;
          PAGE.data.isLock = false;
          PAGE.hightlight(index);
        })
      },
      hightlight: function(index) {
        let swiperItem = document.getElementsByClassName('swiper-pagination-switch');
        for (let i = 0; i < swiperItem.length; i++) {
          swiperItem[i].className = 'swiper-pagination-switch';
        }
        swiperItem[index].className = 'swiper-pagination-switch active';
      },
      animateTo:function(begin,end,duration,changeCallback,finishCallback){
        let startTime = Date.now();
        requestAnimationFrame(function update(){
          let dataNow = Date.now();
          let time = dataNow - startTime;
          let value = PAGE.linear(time,begin,end,duration);
          typeof changeCallback === 'function' && changeCallback(value)
          if(startTime + duration > dataNow){
            requestAnimationFrame(update)
          }else{
            typeof finishCallback === 'function' && finishCallback(end)
          }
        })
      },
      linear: function(time, begin, end, duration) { 
        return ( end - begin ) * time / duration + begin;
      }
    }

    PAGE.init();
  </script>
</body>
</html>