Note! This article is out of date. You can view the upgraded project directly.

Through the study of the last article, we have basically mastered the packaging and development process of a wheel. So this time we will take you to develop a more difficult project – the wheel map, I hope to further deepen our understanding and understanding of object-oriented plug-in development.

So, Let’s begin!

At present, the project uses ES5 and UMD standard packaging, so the front-end only supports the introduction of

Carousel – Mobile Github: CSdWheels Click the Star if you think it works.

Web wheel sow

Thought analysis

As a rule of thumb, you need to have a sense of what you’re developing before you start writing code. For example, you can have a mental overview of what the final project will look like. Here, you can look directly at the animation or the project page to experience it. Before the actual development stage, we should have an overall analysis of the logic of the plug-in, so that the development will be more efficient, and can effectively avoid the problems caused by unclear thinking.

First, let’s take a look at the effects and interactions of Web rotation:

  1. Automatic rotation at regular intervals
  2. Left and right arrows can switch round seeding
  3. Dot can switch round seeding
  4. When the mouse is in the rotation area, the rotation is suspended; After leaving the zone, the rotation resumes
  5. There is a uniform motion animation effect when the rotation is switched
  6. When switching to the right to the last sheet, it will automatically loop to the first sheet; When you switch left to the first card, loop to the last card

The above points, can be said to be a round – cast graph must achieve the classic effect. Ignore the other effects first, sixth is obviously the most difficult for beginners, in fact, this effect has a common name – seamless rotation. “Seamless” can also be understood as an infinite loop, in fact, can let the rotation in one direction always switch, and automatically switch between the end of the picture cycle.

For example, now we have five images, we number them as:

One, two, three, four, five

In order to achieve the effect of the above, you might think of add a judgment when switching to head to tail, mandatory change image position, but if you do, when you finally a switch back to the first graph will appear blank, so also need to add a tail and head the head and tail respectively in transition elements as position changes:

5, 1, 2, 3, 4, 5, 1

With the help of these two images, the above effect can be achieved smoothly. At this point, the analysis of the basic idea of the project is finished, let’s enter the coding stage!

Basic architecture

Before we start, we still need to set up the basic architecture of the project:

(function(root, factory) {
    if (typeof define === "function" && define.amd) {
      define([], factory);
    } else if (typeof module= = ="object" && module.exports) {
      module.exports = factory();
    } else {
      root.Carousel = factory();
    }
  })(typeofself ! = ="undefined" ? self : this.function() {
    "use strict";

    // ID-NAMES
    var ID = {
      CAROUSEL_WRAP: '#carouselWrap'.CAROUSEL_DOTS: '#carouselDots'.ARROW_LEFT: '#arrowLeft'.ARROW_RIGHT: '#arrowRight'
    };

    var CLASS = {
      CAROUSEL_WRAP: 'carousel-wrap'.CAROUSEL_IMG: 'carousel-image'.CAROUSEL_DOTS_WRAP: 'carousel-buttons-wrap'.CAROUSEL_DOTS: 'carousel-buttons'.CAROUSEL_DOT: 'carousel-button'.CAROUSEL_DOT_ON: 'carousel-button on'.CAROUSEL_ARROW_LEFT: 'carousel-arrow arrow-left'.CAROUSEL_ARROW_RIGHT: 'carousel-arrow arrow-right'
    };

    // Polyfills
    function addEvent(element, type, handler) {
      if (element.addEventListener) {
        element.addEventListener(type, handler, false);
      } else if (element.attachEvent) {
        element.attachEvent("on" + type, handler);
      } else {
        element["on"+ type] = handler; }}// Merge objects
    function extend(o, n, override) {
      for (var p in n) {
        if (n.hasOwnProperty(p) && (!o.hasOwnProperty(p) || override))
          o[p] = n[p];
      }
    }

    // rotation - constructor
    var Carousel = function (selector, userOptions) {
      var _this = this;
      // Merge the configuration
      extend(this.carouselOptions, userOptions, true);
      // Get the rotation element
      _this.carousel = document.querySelector(selector);
      // Initializes the multicast list
      _this.carousel.appendChild(_this.getImgs());
      // Get the round list
      _this.carouselWrap = document.querySelector(ID.CAROUSEL_WRAP);
      // check whether the load is complete every 50ms
      var checkInterval = 50;
      var checkTimer = setInterval(function () {
        // Check whether the load is complete
        if (_this.isCarouselComplete()) {
          // Clear timer after loading
          clearInterval(checkTimer);
          // Initialize the rotation
          _this.initCarousel();
          // Initialize the dot
          _this.initDots();
          // Initialize the arrow
          _this.initArrows();
        }
      }, checkInterval);
    };
    // round - prototype object
    Carousel.prototype = {
      carouselOptions: {
        // Whether to display the rotation arrow
        showCarouselArrow: true.// Whether to display round-cast dots
        showCarouselDot: true.// Automatic rotation interval
        carouselInterval: 3000.// Total rotation animation time
        carouselAnimateTime: 150.// Rotate the animation interval
        carouselAnimateInterval: 10
      },
      isCarouselComplete: function () {
        // Check whether the page image is loaded
        var completeCount = 0;
        for (var i = 0; i < this.carouselWrap.children.length; i++) {
          if (this.carouselWrap.children[i].complete) { completeCount++; }}return completeCount === this.carouselWrap.children.length ? true : false; }};return Carousel;
});
Copy the code

The addEvent() and extend() functions were introduced in the previous article, and the various configuration items in the constructor are also used in the project, so needless to say. The checkTimer is used to check whether all picture elements on the page are loaded at regular intervals. If so, the project is initialized. Why do we need to do that? Because our picture elements are dynamically loaded in JS using DOM, some logic statements in JS may be executed before the picture is loaded, leading to the phenomenon that the picture and corresponding element attributes cannot be correctly obtained through DOM API. So in isCarouselComplete() we use the complete attribute of the IMG element to determine if all images on the current page have been loaded, and then ensure that the DOM attribute is retrieved correctly.

Initialize the rotation

Complete initCarousel() :

initCarousel: function(selector, userOptions) {
    // Get the number of rounds
    this.carouselCount = this.carouselWrap.children.length;
    // Set the rotation
    this.setCarousel();
    // Initializes the multicast number
    this.carouselIndex = 1;
    // Initializes the timer
    this.carouselIntervalr = null;
    // Each displacement = total displacement/times
    this.carouselAnimateSpeed = this.carouselWidth / (this.carouselOptions.carouselAnimateTime / this.carouselOptions.carouselAnimateInterval);
    // Check whether the animation is in rotation state
    this.isCarouselAnimate = false;
    // Check whether the dot is clicked
    this.isDotClick = false;
    // Bind the multicast graph event
    this.bindCarousel();
    // Play the rotation
    this.playCarousel();
}
Copy the code

By enclosing carouselWidth/(this. CarouselOptions. CarouselAnimateTime/enclosing carouselOptions. CarouselAnimateInterval) this formula, You can calculate the offset of each rotation animation displacement, which will be used to complete the animation function later.

In setCarousel(), set the basic properties of rotation:

setCarousel: function () {
    // Copy the first and last nodes
    var first = this.carouselWrap.children[0].cloneNode(true);
    var last = this.carouselWrap.children[this.carouselCount - 1].cloneNode(true);
    // Add transition elements
    this.carouselWrap.insertBefore(last, this.carouselWrap.children[0]);
    this.carouselWrap.appendChild(first);
    // Set the rotation width
    this.setWidth(this.carousel, this.carouselOptions.carouselWidth);
    // Set the rotation height
    this.setHeight(this.carousel, this.carouselOptions.carouselHeight);
    // Get the rotation width
    this.carouselWidth = this.getWidth(this.carousel);
    // Set the initial position
    this.setLeft(this.carouselWrap, -this.carouselWidth);
    // Set the rotation length
    this.setWidth(this.carouselWrap, this.carouselWidth * this.carouselWrap.children.length);
}
Copy the code

Add transition elements at the beginning and end, set height and width, etc.

Bind the multicast event

Then there is the binding of mouse in and mouse out events:

playCarousel: function () {
    var _this = this;
    this.carouselIntervalr = window.setInterval(function() {
        _this.nextCarousel();
    }, this.carouselOptions.carouselInterval);
},
bindCarousel: function () {
    var _this = this;
    // Mouse in and out events
    addEvent(this.carousel, 'mouseenter'.function(e) {
        clearInterval(_this.carouselIntervalr);
    });
    addEvent(this.carousel, 'mouseleave'.function(e) {
        _this.playCarousel();
    });
}
Copy the code

Stop the timer of round play when moving in, and automatically start the next one after moving out.

Complete the nextCarousel() and prevCarousel() functions:

prevCarousel: function () {
    if (!this.isCarouselAnimate) {
        // Change the multicast sequence number
        this.carouselIndex--;
        if (this.carouselIndex < 1) {
            this.carouselIndex = this.carouselCount;
        }
        // Set the rotation position
        this.moveCarousel(this.isFirstCarousel(), this.carouselWidth);
        if (this.carouselOptions.showCarouselDot) {
            // Displays the current dot
            this.setDot(); }}},nextCarousel: function () {
    if (!this.isCarouselAnimate) {
        this.carouselIndex++;
        if (this.carouselIndex > this.carouselCount) {
            this.carouselIndex = 1;
        }
        this.moveCarousel(this.isLastCarousel(), -this.carouselWidth);
        if (this.carouselOptions.showCarouselDot) {
            // Displays the current dot
            this.setDot(); }}}Copy the code

The function is the same, change the rotation number, and then move the rotation. Complete the transition elements in moveCarousel() :

moveCarousel: function (status, carouselWidth) {
    var left = 0;
    if (status) {
        left = -this.carouselIndex * this.carouselWidth;
    } else {
        left = this.getLeft(this.carouselWrap) + carouselWidth;
    }
    this.setLeft(this.carouselWrap, left);
}
Copy the code

The Settings of the properties and events related to the rotation are complete.

Bind dot events

Next comes the event binding for the dot:

bindDots: function () {
    var _this = this;
    for (var i = 0, len = this.carouselDots.children.length; i < len; i++) {
        (function(i) {
            addEvent(_this.carouselDots.children[i], 'click'.function (ev) {
                // Get the click dot number
                _this.dotIndex = i + 1;
                if(! _this.isCarouselAnimate && _this.carouselIndex ! == _this.dotIndex) {// Change the dot click state
                _this.isDotClick = true;
                // Change the dot position_this.moveDot(); }}); })(i); }},moveDot: function () {
    // Change the current multicast sequence number
    this.carouselIndex = this.dotIndex;
    // Set the rotation position
    this.setLeft(this.carouselWrap, -this.carouselIndex * this.carouselWidth);
    // Reset the current dot style
    this.setDot();
},
setDot: function () {
    for (var i = 0, len = this.carouselDots.children.length; i < len; i++) {
        this.carouselDots.children[i].setAttribute('class', CLASS.CAROUSEL_DOT);
    }
    this.carouselDots.children[this.carouselIndex - 1].setAttribute('class', CLASS.CAROUSEL_DOT_ON);
}
Copy the code

Function is very simple, click dot, jump to the corresponding serial number of the wheel, reset the style of small dot.

Bind arrow events

Finally, we need to bind arrow events:

bindArrows: function () {
    var _this = this;
    // Arrow click event
    addEvent(this.arrowLeft, 'click'.function(e) {
        _this.prevCarousel();
    });
    addEvent(this.arrowRight, 'click'.function(e) {
        _this.nextCarousel();
    });
}
Copy the code

The result is a seamless rotation without animation, as shown below:

Animation effect

In the last section, the idea after our analysis is basically realized, but how to realize the animation effect when the rotation switch?

To animate, we need to find the source of the animation — the moveCarousel() function that switches the rotation. Therefore, we need to modify it first:

moveCarousel: function (target, speed) {
    var _this = this;
    _this.isCarouselAnimate = true;
    function animateCarousel () {
        if ((speed > 0 && _this.getLeft(_this.carouselWrap) < target) ||
            (speed < 0 && _this.getLeft(_this.carouselWrap) > target)) {
        _this.setLeft(_this.carouselWrap, _this.getLeft(_this.carouselWrap) + speed);
        timer = window.setTimeout(animateCarousel, _this.carouselOptions.carouselAnimateInterval);
        } else {
        window.clearTimeout(timer);
        // Reset the multicast state_this.resetCarousel(target, speed); }}var timer = animateCarousel();
}
Copy the code

The modified moveCarousel() function takes two arguments: target represents the position of the rotation to be moved to, and speed is the value of the offset calculated earlier. The animateCarousel() function is then used to recursively call setTimeout(), simulating the effect of a timer. The recursion continues when the target position is not determined, and the rotation state is reset if it is reached:

// Set the current left value to the target value
this.setLeft(this.carouselWrap, target);
// If you are currently on the auxiliary map, return to the real map
if (target > -this.carouselWidth ) {
    this.setLeft(this.carouselWrap, -this.carouselCount * this.carouselWidth);
}
if (target < (-this.carouselWidth * this.carouselCount)) {
    this.setLeft(this.carouselWrap, -this.carouselWidth);
}
Copy the code

The reset process is the same as the previous implementation and will not be described again. After completing the new moveCarousel() function, we need to modify prevCarousel() and nextCarousel() :

prevCarousel: function () {
    if (!this.isCarouselAnimate) {
        // Change the multicast sequence number
        this.carouselIndex--;
        if (this.carouselIndex < 1) {
            this.carouselIndex = this.carouselCount;
        }
        // Set the rotation position
        this.moveCarousel(this.getLeft(this.carouselWrap) + this.carouselWidth, this.carouselAnimateSpeed);
        if (this.carouselOptions.showCarouselDot) {
            // Displays the current dot
            this.setDot(); }}},nextCarousel: function () {
    if (!this.isCarouselAnimate) {
        this.carouselIndex++;
        if (this.carouselIndex > this.carouselCount) {
            this.carouselIndex = 1;
        }
        this.moveCarousel(this.getLeft(this.carouselWrap) - this.carouselWidth,  -this.carouselAnimateSpeed);
        if (this.carouselOptions.showCarouselDot) {
            // Displays the current dot
            this.setDot(); }}},Copy the code

It simply replaces the arguments to the moveCarousel() call. After the transformation of these functions, the animation effect is preliminarily realized:

Optimize animation

During actual testing on the page, we may occasionally see a lag, mainly due to the simulation of animation after using setTimeout() recursively (it also happens when using setInterval() directly), So we need to use requestAnimationFrame, a new HTML5 API, to optimize animation efficiency, and modify moveCarousel() function again:

moveCarousel: function (target, speed) {
    var _this = this;
    _this.isCarouselAnimate = true;
    function animateCarousel () {
        if ((speed > 0 && _this.getLeft(_this.carouselWrap) < target) ||
            (speed < 0 && _this.getLeft(_this.carouselWrap) > target)) {
            _this.setLeft(_this.carouselWrap, _this.getLeft(_this.carouselWrap) + speed);
            timer = window.requestAnimationFrame(animateCarousel);
        } else {
            window.cancelAnimationFrame(timer);
            // Reset the multicast state_this.resetCarousel(target, speed); }}var timer = window.requestAnimationFrame(animateCarousel);
}
Copy the code

The two methods are called in a similar way, but in reality, the animation is much smoother, and most importantly, it makes our animation efficiency greatly improved.

Is this the end of our development? After the above animation, when you click on the dot, the rotation is jumpy, not as complete as in our initial GIF. A new way of thinking is needed to make the clicking effect of any dot still switch as if it were an adjacent image.

If we are in the first image and the sequence number is 1, and the dot corresponding to the image number is 5, we can do this: Insert a picture node corresponding to no. 5 behind the picture node corresponding to no. 1, and then switch to the new picture in rotation. After the switch, immediately change the position of the picture to the real picture with no. 5, and finally delete the new node, as follows:

Step 1: Insert a new node 5 1 5 2 3 4 5 1

Step 2: Change the position of the picture and keep the order of the nodes unchanged

Step 3: Delete the new node and restore the node sequence 5, 1, 2, 3, 4, 5, 1

This is what it looks like in code:

moveDot: function () {
    // Change the rotation DOM to add transition effects
    this.changeCarousel();
    // Change the current multicast sequence number
    this.carouselIndex = this.dotIndex;
    // Reset the current dot style
    this.setDot();
},
changeCarousel: function () {
    // Save the current node location
    this.currentNode = this.carouselWrap.children[this.carouselIndex];
    // Get the destination node location
    var targetNode = this.carouselWrap.children[this.dotIndex];
    // Determine the click dot relative to the current position
    if (this.carouselIndex < this.dotIndex) {
        // Insert the target node to the right of the current element
        var nextNode = this.currentNode.nextElementSibling;
        this.carouselWrap.insertBefore(targetNode.cloneNode(true), nextNode);
        this.moveCarousel(this.getLeft(this.carouselWrap) - this.carouselWidth, -this.carouselAnimateSpeed);
    }
    if (this.carouselIndex > this.dotIndex) {
        // Insert the target node to the left of the current element
        this.carouselWrap.insertBefore(targetNode.cloneNode(true), this.currentNode);
        // After the node is inserted to the left, the position of the current element is changed, which causes the screen to shake
        this.setLeft(this.carouselWrap, -(this.carouselIndex + 1) * this.carouselWidth);
        this.moveCarousel(this.getLeft(this.carouselWrap) + this.carouselWidth, this.carouselAnimateSpeed); }}Copy the code

It should be noted that the relationship between the dot serial number clicked and the current serial number should be determined, that is, on the left or right of the current serial number. If it is on the left, the position needs to be reset. The last step completes the deletion function resetMoveDot() for the new node:

resetCarousel: function (target, speed) {
    // Check whether the dot is clicked
    if (this.isDotClick) {
        // Reset the status of the dot after it is clicked
        this.resetMoveDot(speed);
    } else {
        // Reset the state of arrow or auto rotation
        this.resetMoveCarousel(target);
    }
    this.isDotClick = false;
    this.isCarouselAnimate = false;
},
resetMoveDot: function (speed) {
    // If the dot click triggers the animation, delete the new transition node and reset the rotation position to the actual position
    this.setLeft(this.carouselWrap, -this.dotIndex * this.carouselWidth);
    // Determine the position of the click dot relative to the current dot
    if (speed < 0) {
        this.carouselWrap.removeChild(this.currentNode.nextElementSibling);
    } else {
        this.carouselWrap.removeChild(this.currentNode.previousElementSibling); }},Copy the code

Check out the results:

Done!!

H5 shuffling

In the implementation of Web version rotation, we directly use the left value after the absolute positioning of elements to control the position. Although this method has good compatibility, its efficiency is relatively low. In the mobile version of the implementation, we can not worry about this compatibility issue, and can achieve animation in a more efficient way.

If you’re familiar with CSS3, you probably know the transform property. It literally means transform, and its values include rotate, skew, scale, move Translate, and matrix transformation. Translate is what we need today. By using it and transition and other animation properties, we can achieve a more efficient and concise image rotation on mobile devices.

Since the basic ideas and architecture are similar to the Web version, and H5 was rewritten based on the Web version, here are just a few changes that need to be made.

Replace the Left operation method

The setLeft() and getLeft() methods need to be overridden, so we’ll just replace them with two new methods:

setLeft: function (elem, value) {
  elem.style.left = value + 'px';
},
getLeft: function (elem) {
  return parseInt(elem.style.left);
}

setTransform: function(elem ,value) {
  elem.style.transform =
    "translate3d(" + value + "px, 0px, 0px)";
  elem.style["-webkit-transform"] =
    "translate3d(" + value + "px, 0px, 0px)";
  elem.style["-ms-transform"] =
    "translate3d(" + value + "px, 0px, 0px)";
},
getTransform: function() {
  var x =
    this.carouselWrap.style.transform ||
    this.carouselWrap.style["-webkit-transform"] | |this.carouselWrap.style["-ms-transform"];
  x = x.substring(12);
  x = x.match(/(\S*)px/) [1];
  return Number(x);
}
Copy the code

The new version of the method function and the old version has been completely, but the implementation of the method used is not the same. Now we need to set the transition value to the requestAnimationFrame attribute:

setTransition: function(elem, value) {
  elem.style.transition = value + 'ms';
}
Copy the code

With these three methods, we can then rewrite the moveCarousel(), resetCarousel(), and resetMoveCarousel() methods:

moveCarousel: function(target) {
  this.isCarouselAnimate = true;
  this.setTransition(this.carouselWrap, this.carouselOptions.carouselDuration);
  this.setTransform(this.carouselWrap, target);
  this.resetCarousel(target);
},
resetCarousel: function(target) {
  var _this = this;
  window.setTimeout(function() {
    // Reset the state of arrow or auto rotation
    _this.resetMoveCarousel(target);
    _this.isCarouselAnimate = false;
  }, _this.carouselOptions.carouselDuration);
},
resetMoveCarousel: function(target) {
  this.setTransition(this.carouselWrap, 0);
  // Set the current left value to the target value
  this.setTransform(this.carouselWrap, target);
  // If you are currently on the auxiliary map, return to the real map
  if (target > -this.carouselWidth) {
    this.setTransform(this.carouselWrap, -this.carouselCount * this.carouselWidth);
  }
  if (target < -this.carouselWidth * this.carouselCount) {
    this.setTransform(this.carouselWrap, -this.carouselWidth); }}Copy the code

The reason we reset transition before setTransform() changes position is because transition animates every position change, and we don’t want the user to see any transitions in our code. It needs to be reset to match the previous implementation.

Add touch event

On mobile, we are used to using our fingers to touch the screen, so the interaction between the dots and arrows on the Web is not so appropriate. Instead, we can change the interaction to touch, which is what the Touch event does:

bindCarousel: function() {
  var _this = this;
  // Mouse in and out events
  addEvent(this.carousel, "touchstart".function(e) {
    if(! _this.isCarouselAnimate) { clearInterval(_this.carouselIntervalr); _this.carouselTouch.startX = _this.getTransform(); _this.carouselTouch.start = e.changedTouches[e.changedTouches.length -1].clientX; }}); addEvent(this.carousel, "touchmove".function(e) {
    if(! _this.isCarouselAnimate && _this.carouselTouch.start ! =- 1) {
      clearInterval(_this.carouselIntervalr);
      _this.carouselTouch.move =
        e.changedTouches[e.changedTouches.length - 1].clientX - _this.carouselTouch.start; _this.setTransform(_this.carouselWrap, _this.carouselTouch.move + _this.carouselTouch.startX); }}); addEvent(this.carousel, "touchend".function(e) {
    if(! _this.isCarouselAnimate && _this.carouselTouch.start ! =- 1) {
      clearInterval(_this.carouselIntervalr);
      _this.setTransform(_this.carouselWrap, _this.carouselTouch.move + _this.carouselTouch.startX);
      var x = _this.getTransform();
      x +=
        _this.carouselTouch.move > 0
          ? _this.carouselWidth * _this.carouselTouch.offset
          : _this.carouselWidth * -_this.carouselTouch.offset;
      _this.carouselIndex = Math.round(x / _this.carouselWidth) * - 1;
      _this.moveCarousel(
        _this.carouselIndex * -_this.carouselWidth
      );
      if (_this.carouselIndex > _this.carouselCount) {
        _this.carouselIndex = 1;
      }
      if (_this.carouselIndex < 1) { _this.carouselIndex = _this.carouselCount; } _this.playCarousel(); }}); }Copy the code

In simple terms, we divide touch events into three processes — start, move, and end. In these three processes, the corresponding logic and operation can be implemented respectively:

  1. Touchmove gets the starting point of the touch
  2. Touchmove computes the offset after the touch
  3. Determine the direction of the offset and change the position of the picture

Through this logic, we can successfully simulate the touch effect of mobile devices:

The article itself is just the overall idea of the project and the key part of the explanation, some details can not be covered, but also please compare the source code to understand learning ~

Finally, there are a lot of great mods out there, but that doesn’t stop us from writing our own version. Because only their own write again, and in the brain to go through their own thinking process, and then in learning some excellent source code and implementation just not meng circle.

So far, the development of our second wheel has been successfully completed. All the source code has been synchronously updated to Github. If you find any bugs or other problems, you can reply to them in the issue of the project. Dig e a hole and run.

Update (2018-8-14)

The Webpack ES6 version has been updated to support ES6 modular introduction.

import { Carousel } from 'csdwheels'
import { CarouselMobile } from 'csdwheels'
Copy the code

For details, see README

To be continued…

Refer to the content

  • How to optimize “visual regression” when the last image rolls back to the first one in JS running light effect?
  • With native javascript to achieve an infinite scrolling wheel cast graph
  • Native JavaScript implements the wheel map
  • Native JS implementation of the wheel – cast graph
  • Hand – in – hand native JS simple rotation map
  • vue-swiper