<commit_before>/*
 * Swipe 2.0
 *
 * Brad Birdsall and Xiuyu Li
 * Copyright 2015, MIT License
 *
*/

function Swipe(container, options) {

  "use strict";

  // utilities
  var noop = function() {}; // simple no operation function
  var offloadFn = function(fn) { setTimeout(fn || noop, 0) }; // offload a functions execution

  // check browser capabilities
  var browser = {
    addEventListener: !!window.addEventListener,
    touch: ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch,
    transitions: (function(temp) {
      var props = ['transitionProperty', 'WebkitTransition', 'MozTransition', 'OTransition', 'msTransition'];
      for ( var i in props ) if (temp.style[ props[i] ] !== undefined) return true;
      return false;
    })(document.createElement('swipe'))
  };

  // quit if no root element
  if (!container) return;
  var element = container.children[0];
  var slides, slidePos, width, length;
  options = options || {};
  var index = parseInt(options.startSlide, 10) || 0;
  var speed = options.speed || 300;
  options.continuous = options.continuous !== undefined ? options.continuous : true;

  // AutoRestart option: auto restart slideshow after user's touch event
  options.autoRestart = options.autoRestart !== undefined ? options.autoRestart : true;

  function setup() {

    // cache slides
    slides = element.children;
    length = slides.length;

    // set continuous to false if only one slide
    if (slides.length < 2) options.continuous = false;

    //special case if two slides
    if (browser.transitions && options.continuous && slides.length < 3) {
      element.appendChild(slides[0].cloneNode(true));
      element.appendChild(element.children[1].cloneNode(true));
      slides = element.children;
    }

    // create an array to store current positions of each slide
    slidePos = new Array(slides.length);

    // determine width of each slide
    width = container.getBoundingClientRect().width || container.offsetWidth;

    element.style.width = (slides.length * width) + 'px';

    // stack elements
    var pos = slides.length;
    while (pos--) {

      var slide = slides[pos];

      slide.style.width = width + 'px';
      slide.setAttribute('data-index', pos);

      if (browser.transitions) {
        slide.style.left = (pos * -width) + 'px';
        move(pos, index > pos ? -width : (index < pos ? width : 0), 0);
      }

    }

    // reposition elements before and after index
    if (options.continuous && browser.transitions) {
      move(circle(index-1), -width, 0);
      move(circle(index+1), width, 0);
    }

    if (!browser.transitions) element.style.left = (index * -width) + 'px';

    container.style.visibility = 'visible';

  }

  function prev() {

    if (options.continuous) slide(index-1);
    else if (index) slide(index-1);

  }

  function next() {

    if (options.continuous) slide(index+1);
    else if (index < slides.length - 1) slide(index+1);

  }

  function circle(index) {

    // a simple positive modulo using slides.length
    return (slides.length + (index % slides.length)) % slides.length;

  }

  function slide(to, slideSpeed) {

    // do nothing if already on requested slide
    if (index == to) return;

    if (browser.transitions) {

      var direction = Math.abs(index-to) / (index-to); // 1: backward, -1: forward

      // get the actual position of the slide
      if (options.continuous) {
        var natural_direction = direction;
        direction = -slidePos[circle(to)] / width;

        // if going forward but to < index, use to = slides.length + to
        // if going backward but to > index, use to = -slides.length + to
        if (direction !== natural_direction) to =  -direction * slides.length + to;

      }

      var diff = Math.abs(index-to) - 1;

      // move all the slides between index and to in the right direction
      while (diff--) move( circle((to > index ? to : index) - diff - 1), width * direction, 0);

      to = circle(to);

      move(index, width * direction, slideSpeed || speed);
      move(to, 0, slideSpeed || speed);

      if (options.continuous) move(circle(to - direction), -(width * direction), 0); // we need to get the next in place

    } else {

      to = circle(to);
      animate(index * -width, to * -width, slideSpeed || speed);
      //no fallback for a circular continuous if the browser does not accept transitions
    }

    index = to;
    offloadFn(options.callback && options.callback(index, slides[index]));
  }

  function move(index, dist, speed) {

    translate(index, dist, speed);
    slidePos[index] = dist;

  }

  function translate(index, dist, speed) {

    var slide = slides[index];
    var style = slide && slide.style;

    if (!style) return;

    style.webkitTransitionDuration =
    style.MozTransitionDuration =
    style.msTransitionDuration =
    style.OTransitionDuration =
    style.transitionDuration = speed + 'ms';

    style.webkitTransform = 'translate(' + dist + 'px,0)' + 'translateZ(0)';
    style.msTransform =
    style.MozTransform =
    style.OTransform = 'translateX(' + dist + 'px)';

  }

  function animate(from, to, speed) {

    // if not an animation, just reposition
    if (!speed) {

      element.style.left = to + 'px';
      return;

    }

    var start = +new Date;

    var timer = setInterval(function() {

      var timeElap = +new Date - start;

      if (timeElap > speed) {

        element.style.left = to + 'px';

        if (delay) begin();

        options.transitionEnd && options.transitionEnd.call(event, index, slides[index]);

        clearInterval(timer);
        return;

      }

      element.style.left = (( (to - from) * (Math.floor((timeElap / speed) * 100) / 100) ) + from) + 'px';

    }, 4);

  }

  // setup auto slideshow
  var delay = options.auto || 0;
  var interval;

  function begin() {

    interval = setTimeout(next, delay);

  }

  function stop() {

    delay = 0;
    clearTimeout(interval);

  }

  function restart() {
    stop();
    delay = options.auto || 0;
    begin();
  }

  // setup initial vars
  var start = {};
  var delta = {};
  var isScrolling;

  // setup event capturing
  var events = {

    handleEvent: function(event) {

      switch (event.type) {
        case 'touchstart': this.start(event); break;
        case 'touchmove': this.move(event); break;
        case 'touchend': offloadFn(this.end(event)); break;
        case 'webkitTransitionEnd':
        case 'msTransitionEnd':
        case 'oTransitionEnd':
        case 'otransitionend':
        case 'transitionend': offloadFn(this.transitionEnd(event)); break;
        case 'resize': offloadFn(setup); break;
      }

      if (options.stopPropagation) event.stopPropagation();

    },
    start: function(event) {

      var touches = event.touches[0];

      // measure start values
      start = {

        // get initial touch coords
        x: touches.pageX,
        y: touches.pageY,

        // store time to determine touch duration
        time: +new Date

      };

      // used for testing first move event
      isScrolling = undefined;

      // reset delta and end measurements
      delta = {};

      // attach touchmove and touchend listeners
      element.addEventListener('touchmove', this, false);
      element.addEventListener('touchend', this, false);

    },
    move: function(event) {

      // ensure swiping with one touch and not pinching
      if ( event.touches.length > 1 || event.scale && event.scale !== 1) return

      if (options.disableScroll) event.preventDefault();

      var touches = event.touches[0];

      // measure change in x and y
      delta = {
        x: touches.pageX - start.x,
        y: touches.pageY - start.y
      };

      // determine if scrolling test has run - one time test
      if ( typeof isScrolling === 'undefined') {
        isScrolling = !!( isScrolling || Math.abs(delta.x) < Math.abs(delta.y) );
      }

      // if user is not trying to scroll vertically
      if (!isScrolling) {

        // prevent native scrolling
        event.preventDefault();

        // stop slideshow
        stop();

        // increase resistance if first or last slide
        if (options.continuous) { // we don't add resistance at the end

          translate(circle(index-1), delta.x + slidePos[circle(index-1)], 0);
          translate(index, delta.x + slidePos[index], 0);
          translate(circle(index+1), delta.x + slidePos[circle(index+1)], 0);

        } else {

          delta.x =
            delta.x /
              ( (!index && delta.x > 0               // if first slide and sliding left
                || index == slides.length - 1        // or if last slide and sliding right
                && delta.x < 0                       // and if sliding at all
              ) ?
              ( Math.abs(delta.x) / width + 1 )      // determine resistance level
              : 1 );                                 // no resistance if false

          // translate 1:1
          translate(index-1, delta.x + slidePos[index-1], 0);
          translate(index, delta.x + slidePos[index], 0);
          translate(index+1, delta.x + slidePos[index+1], 0);
        }

      }

    },
    end: function(event) {

      // measure duration
      var duration = +new Date - start.time;

      // determine if slide attempt triggers next/prev slide
      var isValidSlide =
            Number(duration) < 250               // if slide duration is less than 250ms
            && Math.abs(delta.x) > 20            // and if slide amt is greater than 20px
            || Math.abs(delta.x) > width/2;      // or if slide amt is greater than half the width

      // determine if slide attempt is past start and end
      var isPastBounds =
            !index && delta.x > 0                            // if first slide and slide amt is greater than 0
            || index == slides.length - 1 && delta.x < 0;    // or if last slide and slide amt is less than 0

      if (options.continuous) isPastBounds = false;

      // determine direction of swipe (true:right, false:left)
      var direction = delta.x < 0;

      // if not scrolling vertically
      if (!isScrolling) {

        if (isValidSlide && !isPastBounds) {

          if (direction) {

            if (options.continuous) { // we need to get the next in this direction in place

              move(circle(index-1), -width, 0);
              move(circle(index+2), width, 0);

            } else {
              move(index-1, -width, 0);
            }

            move(index, slidePos[index]-width, speed);
            move(circle(index+1), slidePos[circle(index+1)]-width, speed);
            index = circle(index+1);

          } else {
            if (options.continuous) { // we need to get the next in this direction in place

              move(circle(index+1), width, 0);
              move(circle(index-2), -width, 0);

            } else {
              move(index+1, width, 0);
            }

            move(index, slidePos[index]+width, speed);
            move(circle(index-1), slidePos[circle(index-1)]+width, speed);
            index = circle(index-1);

          }

          options.callback && options.callback(index, slides[index]);

        } else {

          if (options.continuous) {

            move(circle(index-1), -width, speed);
            move(index, 0, speed);
            move(circle(index+1), width, speed);

          } else {

            move(index-1, -width, speed);
            move(index, 0, speed);
            move(index+1, width, speed);
          }

        }

      }

      // kill touchmove and touchend event listeners until touchstart called again
      element.removeEventListener('touchmove', events, false)
      element.removeEventListener('touchend', events, false)

    },
    transitionEnd: function(event) {

      if (parseInt(event.target.getAttribute('data-index'), 10) == index) {

        if (delay || options.autoRestart) restart();

        options.transitionEnd && options.transitionEnd.call(event, index, slides[index]);

      }

    }

  };

  // trigger setup
  setup();

  // start auto slideshow if applicable
  if (delay) begin();

  // add event listeners
  if (browser.addEventListener) {

    // set touchstart event on element
    if (browser.touch) element.addEventListener('touchstart', events, false);

    if (browser.transitions) {
      element.addEventListener('webkitTransitionEnd', events, false);
      element.addEventListener('msTransitionEnd', events, false);
      element.addEventListener('oTransitionEnd', events, false);
      element.addEventListener('otransitionend', events, false);
      element.addEventListener('transitionend', events, false);
    }

    // set resize event on window
    window.addEventListener('resize', events, false);

  } else {

    window.onresize = function () { setup() }; // to play nice with old IE

  }

  // expose the Swipe API
  return {
    setup: function() {

      setup();

    },
    slide: function(to, speed) {

      // cancel slideshow
      stop();

      slide(to, speed);

    },
    prev: function() {

      // cancel slideshow
      stop();

      prev();

    },
    next: function() {

      // cancel slideshow
      stop();

      next();

    },
    restart: function() {

      // Restart slideshow
      restart();

    },
    stop: function() {

      // cancel slideshow
      stop();

    },
    getPos: function() {

      // return current index position
      return index;

    },
    getNumSlides: function() {

      // return total number of slides
      return length;
    },
    kill: function() {

      // cancel slideshow
      stop();

      // reset element
      element.style.width = '';
      element.style.left = '';

      // reset slides
      var pos = slides.length;
      while (pos--) {

        var slide = slides[pos];
        slide.style.width = '';
        slide.style.left = '';

        if (browser.transitions) translate(pos, 0, 0);

      }

      // removed event listeners
      if (browser.addEventListener) {

        // remove current event listeners
        element.removeEventListener('touchstart', events, false);
        element.removeEventListener('webkitTransitionEnd', events, false);
        element.removeEventListener('msTransitionEnd', events, false);
        element.removeEventListener('oTransitionEnd', events, false);
        element.removeEventListener('otransitionend', events, false);
        element.removeEventListener('transitionend', events, false);
        window.removeEventListener('resize', events, false);

      }
      else {

        window.onresize = null;

      }

    }
  }

}

(function (factory) {
  if(typeof module === "object" && typeof module.exports === "object") {
    factory(require("jquery"), window, document);
  } else {
    factory(jQuery, window, document);
  }
}(function($, window, document, undefined) {
  if ( window.jQuery || window.Zepto ) {
    (function($) {
      $.fn.Swipe = function(params) {
        return this.each(function() {
          $(this).data('Swipe', new Swipe($(this)[0], params));
        });
      }
    })( window.jQuery || window.Zepto )
  }
}));
<commit_msg>*Fix: Auto scroll always starts, even the option is not defined<commit_after>/*
 * Swipe 2.0
 *
 * Brad Birdsall and Xiuyu Li
 * Copyright 2015, MIT License
 *
*/

function Swipe(container, options) {

  "use strict";

  // utilities
  var noop = function() {}; // simple no operation function
  var offloadFn = function(fn) { setTimeout(fn || noop, 0) }; // offload a functions execution

  // check browser capabilities
  var browser = {
    addEventListener: !!window.addEventListener,
    touch: ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch,
    transitions: (function(temp) {
      var props = ['transitionProperty', 'WebkitTransition', 'MozTransition', 'OTransition', 'msTransition'];
      for ( var i in props ) if (temp.style[ props[i] ] !== undefined) return true;
      return false;
    })(document.createElement('swipe'))
  };

  // quit if no root element
  if (!container) return;
  var element = container.children[0];
  var slides, slidePos, width, length;
  options = options || {};
  var index = parseInt(options.startSlide, 10) || 0;
  var speed = options.speed || 300;
  options.continuous = options.continuous !== undefined ? options.continuous : true;

  // AutoRestart option: auto restart slideshow after user's touch event
  options.autoRestart = options.autoRestart !== undefined ? options.autoRestart : true;

  function setup() {

    // cache slides
    slides = element.children;
    length = slides.length;

    // set continuous to false if only one slide
    if (slides.length < 2) options.continuous = false;

    //special case if two slides
    if (browser.transitions && options.continuous && slides.length < 3) {
      element.appendChild(slides[0].cloneNode(true));
      element.appendChild(element.children[1].cloneNode(true));
      slides = element.children;
    }

    // create an array to store current positions of each slide
    slidePos = new Array(slides.length);

    // determine width of each slide
    width = container.getBoundingClientRect().width || container.offsetWidth;

    element.style.width = (slides.length * width) + 'px';

    // stack elements
    var pos = slides.length;
    while (pos--) {

      var slide = slides[pos];

      slide.style.width = width + 'px';
      slide.setAttribute('data-index', pos);

      if (browser.transitions) {
        slide.style.left = (pos * -width) + 'px';
        move(pos, index > pos ? -width : (index < pos ? width : 0), 0);
      }

    }

    // reposition elements before and after index
    if (options.continuous && browser.transitions) {
      move(circle(index-1), -width, 0);
      move(circle(index+1), width, 0);
    }

    if (!browser.transitions) element.style.left = (index * -width) + 'px';

    container.style.visibility = 'visible';

  }

  function prev() {

    if (options.continuous) slide(index-1);
    else if (index) slide(index-1);

  }

  function next() {

    if (options.continuous) slide(index+1);
    else if (index < slides.length - 1) slide(index+1);

  }

  function circle(index) {

    // a simple positive modulo using slides.length
    return (slides.length + (index % slides.length)) % slides.length;

  }

  function slide(to, slideSpeed) {

    // do nothing if already on requested slide
    if (index == to) return;

    if (browser.transitions) {

      var direction = Math.abs(index-to) / (index-to); // 1: backward, -1: forward

      // get the actual position of the slide
      if (options.continuous) {
        var natural_direction = direction;
        direction = -slidePos[circle(to)] / width;

        // if going forward but to < index, use to = slides.length + to
        // if going backward but to > index, use to = -slides.length + to
        if (direction !== natural_direction) to =  -direction * slides.length + to;

      }

      var diff = Math.abs(index-to) - 1;

      // move all the slides between index and to in the right direction
      while (diff--) move( circle((to > index ? to : index) - diff - 1), width * direction, 0);

      to = circle(to);

      move(index, width * direction, slideSpeed || speed);
      move(to, 0, slideSpeed || speed);

      if (options.continuous) move(circle(to - direction), -(width * direction), 0); // we need to get the next in place

    } else {

      to = circle(to);
      animate(index * -width, to * -width, slideSpeed || speed);
      //no fallback for a circular continuous if the browser does not accept transitions
    }

    index = to;
    offloadFn(options.callback && options.callback(index, slides[index]));
  }

  function move(index, dist, speed) {

    translate(index, dist, speed);
    slidePos[index] = dist;

  }

  function translate(index, dist, speed) {

    var slide = slides[index];
    var style = slide && slide.style;

    if (!style) return;

    style.webkitTransitionDuration =
    style.MozTransitionDuration =
    style.msTransitionDuration =
    style.OTransitionDuration =
    style.transitionDuration = speed + 'ms';

    style.webkitTransform = 'translate(' + dist + 'px,0)' + 'translateZ(0)';
    style.msTransform =
    style.MozTransform =
    style.OTransform = 'translateX(' + dist + 'px)';

  }

  function animate(from, to, speed) {

    // if not an animation, just reposition
    if (!speed) {

      element.style.left = to + 'px';
      return;

    }

    var start = +new Date;

    var timer = setInterval(function() {

      var timeElap = +new Date - start;

      if (timeElap > speed) {

        element.style.left = to + 'px';

        if (delay) begin();

        options.transitionEnd && options.transitionEnd.call(event, index, slides[index]);

        clearInterval(timer);
        return;

      }

      element.style.left = (( (to - from) * (Math.floor((timeElap / speed) * 100) / 100) ) + from) + 'px';

    }, 4);

  }

  // setup auto slideshow
  var delay = options.auto || 0;
  var interval;

  function begin() {
    if(delay > 0) {
      interval = setTimeout(next, delay);
    }
  }

  function stop() {

    delay = 0;
    clearTimeout(interval);

  }

  function restart() {
    stop();
    delay = options.auto || 0;
    begin();
  }

  // setup initial vars
  var start = {};
  var delta = {};
  var isScrolling;

  // setup event capturing
  var events = {

    handleEvent: function(event) {

      switch (event.type) {
        case 'touchstart': this.start(event); break;
        case 'touchmove': this.move(event); break;
        case 'touchend': offloadFn(this.end(event)); break;
        case 'webkitTransitionEnd':
        case 'msTransitionEnd':
        case 'oTransitionEnd':
        case 'otransitionend':
        case 'transitionend': offloadFn(this.transitionEnd(event)); break;
        case 'resize': offloadFn(setup); break;
      }

      if (options.stopPropagation) event.stopPropagation();

    },
    start: function(event) {

      var touches = event.touches[0];

      // measure start values
      start = {

        // get initial touch coords
        x: touches.pageX,
        y: touches.pageY,

        // store time to determine touch duration
        time: +new Date

      };

      // used for testing first move event
      isScrolling = undefined;

      // reset delta and end measurements
      delta = {};

      // attach touchmove and touchend listeners
      element.addEventListener('touchmove', this, false);
      element.addEventListener('touchend', this, false);

    },
    move: function(event) {

      // ensure swiping with one touch and not pinching
      if ( event.touches.length > 1 || event.scale && event.scale !== 1) return

      if (options.disableScroll) event.preventDefault();

      var touches = event.touches[0];

      // measure change in x and y
      delta = {
        x: touches.pageX - start.x,
        y: touches.pageY - start.y
      };

      // determine if scrolling test has run - one time test
      if ( typeof isScrolling === 'undefined') {
        isScrolling = !!( isScrolling || Math.abs(delta.x) < Math.abs(delta.y) );
      }

      // if user is not trying to scroll vertically
      if (!isScrolling) {

        // prevent native scrolling
        event.preventDefault();

        // stop slideshow
        stop();

        // increase resistance if first or last slide
        if (options.continuous) { // we don't add resistance at the end

          translate(circle(index-1), delta.x + slidePos[circle(index-1)], 0);
          translate(index, delta.x + slidePos[index], 0);
          translate(circle(index+1), delta.x + slidePos[circle(index+1)], 0);

        } else {

          delta.x =
            delta.x /
              ( (!index && delta.x > 0               // if first slide and sliding left
                || index == slides.length - 1        // or if last slide and sliding right
                && delta.x < 0                       // and if sliding at all
              ) ?
              ( Math.abs(delta.x) / width + 1 )      // determine resistance level
              : 1 );                                 // no resistance if false

          // translate 1:1
          translate(index-1, delta.x + slidePos[index-1], 0);
          translate(index, delta.x + slidePos[index], 0);
          translate(index+1, delta.x + slidePos[index+1], 0);
        }

      }

    },
    end: function(event) {

      // measure duration
      var duration = +new Date - start.time;

      // determine if slide attempt triggers next/prev slide
      var isValidSlide =
            Number(duration) < 250               // if slide duration is less than 250ms
            && Math.abs(delta.x) > 20            // and if slide amt is greater than 20px
            || Math.abs(delta.x) > width/2;      // or if slide amt is greater than half the width

      // determine if slide attempt is past start and end
      var isPastBounds =
            !index && delta.x > 0                            // if first slide and slide amt is greater than 0
            || index == slides.length - 1 && delta.x < 0;    // or if last slide and slide amt is less than 0

      if (options.continuous) isPastBounds = false;

      // determine direction of swipe (true:right, false:left)
      var direction = delta.x < 0;

      // if not scrolling vertically
      if (!isScrolling) {

        if (isValidSlide && !isPastBounds) {

          if (direction) {

            if (options.continuous) { // we need to get the next in this direction in place

              move(circle(index-1), -width, 0);
              move(circle(index+2), width, 0);

            } else {
              move(index-1, -width, 0);
            }

            move(index, slidePos[index]-width, speed);
            move(circle(index+1), slidePos[circle(index+1)]-width, speed);
            index = circle(index+1);

          } else {
            if (options.continuous) { // we need to get the next in this direction in place

              move(circle(index+1), width, 0);
              move(circle(index-2), -width, 0);

            } else {
              move(index+1, width, 0);
            }

            move(index, slidePos[index]+width, speed);
            move(circle(index-1), slidePos[circle(index-1)]+width, speed);
            index = circle(index-1);

          }

          options.callback && options.callback(index, slides[index]);

        } else {

          if (options.continuous) {

            move(circle(index-1), -width, speed);
            move(index, 0, speed);
            move(circle(index+1), width, speed);

          } else {

            move(index-1, -width, speed);
            move(index, 0, speed);
            move(index+1, width, speed);
          }

        }

      }

      // kill touchmove and touchend event listeners until touchstart called again
      element.removeEventListener('touchmove', events, false)
      element.removeEventListener('touchend', events, false)

    },
    transitionEnd: function(event) {

      if (parseInt(event.target.getAttribute('data-index'), 10) == index) {

        if (delay || options.autoRestart) restart();

        options.transitionEnd && options.transitionEnd.call(event, index, slides[index]);

      }

    }

  };

  // trigger setup
  setup();

  // start auto slideshow if applicable
  if (delay) begin();

  // add event listeners
  if (browser.addEventListener) {

    // set touchstart event on element
    if (browser.touch) element.addEventListener('touchstart', events, false);

    if (browser.transitions) {
      element.addEventListener('webkitTransitionEnd', events, false);
      element.addEventListener('msTransitionEnd', events, false);
      element.addEventListener('oTransitionEnd', events, false);
      element.addEventListener('otransitionend', events, false);
      element.addEventListener('transitionend', events, false);
    }

    // set resize event on window
    window.addEventListener('resize', events, false);

  } else {

    window.onresize = function () { setup() }; // to play nice with old IE

  }

  // expose the Swipe API
  return {
    setup: function() {

      setup();

    },
    slide: function(to, speed) {

      // cancel slideshow
      stop();

      slide(to, speed);

    },
    prev: function() {

      // cancel slideshow
      stop();

      prev();

    },
    next: function() {

      // cancel slideshow
      stop();

      next();

    },
    restart: function() {

      // Restart slideshow
      restart();

    },
    stop: function() {

      // cancel slideshow
      stop();

    },
    getPos: function() {

      // return current index position
      return index;

    },
    getNumSlides: function() {

      // return total number of slides
      return length;
    },
    kill: function() {

      // cancel slideshow
      stop();

      // reset element
      element.style.width = '';
      element.style.left = '';

      // reset slides
      var pos = slides.length;
      while (pos--) {

        var slide = slides[pos];
        slide.style.width = '';
        slide.style.left = '';

        if (browser.transitions) translate(pos, 0, 0);

      }

      // removed event listeners
      if (browser.addEventListener) {

        // remove current event listeners
        element.removeEventListener('touchstart', events, false);
        element.removeEventListener('webkitTransitionEnd', events, false);
        element.removeEventListener('msTransitionEnd', events, false);
        element.removeEventListener('oTransitionEnd', events, false);
        element.removeEventListener('otransitionend', events, false);
        element.removeEventListener('transitionend', events, false);
        window.removeEventListener('resize', events, false);

      }
      else {

        window.onresize = null;

      }

    }
  }

}

(function (factory) {
  if(typeof module === "object" && typeof module.exports === "object") {
    factory(require("jquery"), window, document);
  } else {
    factory(jQuery, window, document);
  }
}(function($, window, document, undefined) {
  if ( window.jQuery || window.Zepto ) {
    (function($) {
      $.fn.Swipe = function(params) {
        return this.each(function() {
          $(this).data('Swipe', new Swipe($(this)[0], params));
        });
      }
    })( window.jQuery || window.Zepto )
  }
}));
<|endoftext|>


<commit_before>/* global Cache */
const config = require('./data/client.json')
const Util = require('./Util')

const request = require('request-promise').defaults({pool: {maxSockets: Infinity}})

let DiscordServer

/**
 * A representation of a Discord guild member.
 * @class DiscordMember
 */
class DiscordMember {
  /**
   * Creates an instance of DiscordMember.
   * @param {DiscordServer} discordServer The DiscordServer to which this DiscordMember belongs
   * @param {Snowflake} id The user id
   * @hideconstructor
   */
  constructor (discordServer, id) {
    this.discordServer = discordServer
    this.id = id

    this.bot = this.discordServer.bot
    this.server = this.discordServer.server

    // Have to do this to prevent circular reference in file requires
    // Gets a reference to DiscordServer so we can run static methods
    DiscordServer = this.discordServer.constructor
  }

  /**
   * Creates, prepares, and returns a new DiscordMember
   *
   * @static
   * @param {DiscordServer} discordServer The DiscordServer to which this DiscordMember belongs
   * @param {Snowflake} id The user id
   * @returns {Promise<DiscordMember>} The newly created and prepared DiscordMember
   * @memberof DiscordMember
   */
  static async new (discordServer, id) {
    let discordMember = new DiscordMember(discordServer, id)

    if (!await discordMember.prepareMember()) {
      return false
    }

    return discordMember
  }

  /**
   * Fetches the user and member from Discord
   * @returns {Promise<boolean>} True if the member exists
   * @memberof DiscordMember
   */
  async prepareMember () {
    try {
      this.user = await this.bot.fetchUser(this.id, false)
      this.member = await this.server.fetchMember(this.user, false)
      return true
    } catch (e) {
      if (config.loud) console.log(`prepareMember: ${e.message}; ${this.id}; ${this.user}`)
      return false
    }
  }

  /**
   * Gets a member's nickname, formatted with this server's specific settings.
   *
   * @param {object} data String replacement data
   * @returns {string} The formatted nickname
   * @memberof DiscordMember
   */
  getNickname (data) {
    return Util.formatDataString(this.discordServer.getSetting('nicknameFormat'), data, this.member)
  }

  /**
   * Called to update the state of a specific member in this Discord server.
   *
   * @param {object} options Options for the verification
   * @param {boolean} options.announce Whether or not to announce this verification
   * @param {boolean} options.clearBindingsCache Whether or not to clear the bindings cache before verifying
   * @returns {object} VerificationResult
   * @returns {boolean} VerificationResult.status - Whether or not the verification was successful
   * @returns {boolean} VerificationResult.error - What went wrong
   * @returns {boolean} VerificationResult.nonFatal - If the verification error was fatal (e.g., this error would occur on any guild, not just this one)
   * @memberof DiscordMember
   */
  async verify (options) {
    options = options || {}
    let data = {}
    let freshData = false

    if (!this.member && !await this.prepareMember()) {
      return {
        status: false,
        error: 'User not in guild.',
        nonFatal: true
      }
    }

    if (this.user.bot) {
      return {
        status: false,
        error: 'RoVer cannot verify bots. :no_entry_sign::robot:'
      }
    }

    if (this.member.roles.find('name', 'RoVer Bypass')) {
      return {
        status: false,
        error: 'RoVer cannot act on users with the "RoVer Bypass" role. :no_entry_sign:',
        nonFatal: true
      }
    }

    try {
      // Read user data from memory, or request it if there isn't any cached.
      data = await Cache.get('users', this.id)
      if (!data) {
        data = await request({
          uri: `https://verify.eryn.io/api/user/${this.id}`,
          json: true,
          simple: false
        })
        freshData = true
      }
    } catch (e) {
      if (config.loud) console.log(e)
      return {
        status: false,
        error: 'Unknown error.'
      }
    }

    // If the status is ok, the user is in the database.
    if (data.status === 'ok') {
      // We only want to update the username if this data isn't from the cache.
      if (freshData) {
        // Resolve the Roblox username from the user id.
        let apiUserData = {}
        try {
          apiUserData = await request({
            uri: `http://api.roblox.com/users/${data.robloxId}`,
            json: true,
            simple: false
          })
        } catch (e) {
          return false
        }

        if (apiUserData.Username) {
          data.robloxUsername = apiUserData.Username
        }
      }

      // Cache the data for future use.
      Cache.set('users', this.id, data)

      // Check if these settings are enabled for this specific server,
      // if so, then put the member in the correct state.

      if (this.discordServer.getSetting('verifiedRole')) {
        let role = this.discordServer.getSetting('verifiedRole')
        if (!this.member.roles.get(role)) {
          try {
            await this.member.addRole(role)
          } catch (e) {
            if (config.loud) console.log(e)
            return {
              status: false,
              nonFatal: true,
              error: "RoVer doesn't have permissions to add roles to that user."
            }
          }
        }
      }

      if (this.discordServer.getSetting('verifiedRemovedRole')) {
        let role = this.discordServer.getSetting('verifiedRemovedRole')
        if (this.member.roles.get(role)) {
          try {
            await this.member.removeRole(role)
          } catch (e) {
            if (config.loud) console.log(e)
            return {
              status: false,
              nonFatal: true,
              error: "RoVer doesn't have permission to remove roles from that user."
            }
          }
        }
      }

      if (this.discordServer.getSetting('nicknameUsers')) {
        let nickname = this.getNickname(data).substring(0, 32)
        if (this.member.displayName !== nickname) {
          try {
            await this.member.setNickname(nickname)
          } catch (e) {
            if (config.loud) console.log(e)
            return {
              status: false,
              nonFatal: true,
              error: this.member.guild.ownerID === this.member.id ? "Sorry, RoVer can't change the server owner's nickname due to Discord permission restrictions. Please manually update your nickname. Or don't, I'm just an error message." : "RoVer doesn't have permission to change that user's nickname."
            }
          }
        }
      }

      if (this.discordServer.getSetting('announceChannel') && options.announce !== false) {
        let channel = await this.server.channels.get(this.discordServer.getSetting('announceChannel'))

        if (channel) {
          try {
            channel.send(`**User verified:** <@${this.id}> as ${data.robloxUsername}`)
          } catch (e) {}
        }
      }

      // Check if we want to resolve group rank bindings with cached or fresh data.
      if (options.clearBindingsCache !== false) {
        await Cache.clear(`bindings.${data.robloxId}`)
      }

      // Resolve group rank bindings for this member.
      if (this.discordServer.getSetting('groupRankBindings').length > 0) {
        for (let binding of this.discordServer.getSetting('groupRankBindings')) {
          // We use a Promise.then here so that they all execute asynchronously.
          DiscordServer.resolveGroupRankBinding(binding, data.robloxId, data.robloxUsername)
            .then((state) => {
              let hasRole = this.member.roles.get(binding.role) != null
              if (hasRole === state) return

              if (state === true) {
                this.member.addRole(binding.role).catch(e => {})
              } else {
                this.member.removeRole(binding.role).catch(e => {})
              }
            })
            .catch(e => {
              if (config.loud) console.log(e)
              console.log('Resolution error for binding')
            })
        }
      }

      return {
        status: true,
        robloxUsername: data.robloxUsername,
        robloxId: data.robloxId,
        discordId: this.member.id,
        discordName: this.member.user.username
      }
    } else {
      // Status was not "ok".
      switch (data.errorCode) {
        case 404:
        // User isn't in the database.

        // Add the "Not Verified" role to the user.
          if (this.discordServer.getSetting('verifiedRemovedRole')) {
            try {
              await this.member.addRole(this.discordServer.getSetting('verifiedRemovedRole'))
            } catch (e) {}
          }

          return {
            status: false,
            error: 'You don\'t seem to be verified! Please go to https://verify.eryn.io and follow the instructions on the page in order to get verified.'
          }
        case 429:
        // This client has exceeded the amount of requests allowed in a 60 second period.
          return {
            status: false,
            error: 'Server is busy. Please try again later.'
          }
        default:
        // Something else is wrong.
          return {
            status: false,
            error: 'Unknown error.'
          }
      }
    }
  }
}

module.exports = DiscordMember
<commit_msg>Fix #70<commit_after>/* global Cache */
const config = require('./data/client.json')
const Util = require('./Util')

const request = require('request-promise').defaults({pool: {maxSockets: Infinity}})

let DiscordServer

/**
 * A representation of a Discord guild member.
 * @class DiscordMember
 */
class DiscordMember {
  /**
   * Creates an instance of DiscordMember.
   * @param {DiscordServer} discordServer The DiscordServer to which this DiscordMember belongs
   * @param {Snowflake} id The user id
   * @hideconstructor
   */
  constructor (discordServer, id) {
    this.discordServer = discordServer
    this.id = id

    this.bot = this.discordServer.bot
    this.server = this.discordServer.server

    // Have to do this to prevent circular reference in file requires
    // Gets a reference to DiscordServer so we can run static methods
    DiscordServer = this.discordServer.constructor
  }

  /**
   * Creates, prepares, and returns a new DiscordMember
   *
   * @static
   * @param {DiscordServer} discordServer The DiscordServer to which this DiscordMember belongs
   * @param {Snowflake} id The user id
   * @returns {Promise<DiscordMember>} The newly created and prepared DiscordMember
   * @memberof DiscordMember
   */
  static async new (discordServer, id) {
    let discordMember = new DiscordMember(discordServer, id)

    if (!await discordMember.prepareMember()) {
      return false
    }

    return discordMember
  }

  /**
   * Fetches the user and member from Discord
   * @returns {Promise<boolean>} True if the member exists
   * @memberof DiscordMember
   */
  async prepareMember () {
    try {
      this.user = await this.bot.fetchUser(this.id, false)
      this.member = await this.server.fetchMember(this.user, false)
      return true
    } catch (e) {
      if (config.loud) console.log(`prepareMember: ${e.message}; ${this.id}; ${this.user}`)
      return false
    }
  }

  /**
   * Gets a member's nickname, formatted with this server's specific settings.
   *
   * @param {object} data String replacement data
   * @returns {string} The formatted nickname
   * @memberof DiscordMember
   */
  getNickname (data) {
    return Util.formatDataString(this.discordServer.getSetting('nicknameFormat'), data, this.member)
  }

  /**
   * Called to update the state of a specific member in this Discord server.
   *
   * @param {object} options Options for the verification
   * @param {boolean} options.announce Whether or not to announce this verification
   * @param {boolean} options.clearBindingsCache Whether or not to clear the bindings cache before verifying
   * @returns {object} VerificationResult
   * @returns {boolean} VerificationResult.status - Whether or not the verification was successful
   * @returns {boolean} VerificationResult.error - What went wrong
   * @returns {boolean} VerificationResult.nonFatal - If the verification error was fatal (e.g., this error would occur on any guild, not just this one)
   * @memberof DiscordMember
   */
  async verify (options) {
    options = options || {}
    let data = {}
    let freshData = false
    let errorAppend = ''

    if (!this.member && !await this.prepareMember()) {
      return {
        status: false,
        error: 'User not in guild.',
        nonFatal: true
      }
    }

    if (this.user.bot) {
      return {
        status: false,
        error: 'RoVer cannot verify bots. :no_entry_sign::robot:'
      }
    }

    if (this.member.roles.find('name', 'RoVer Bypass')) {
      return {
        status: false,
        error: 'RoVer cannot act on users with the "RoVer Bypass" role. :no_entry_sign:',
        nonFatal: true
      }
    }

    if ((await this.server.fetchMember(this.bot.user.id)).highestRole.comparePositionTo(this.member.highestRole) < 1) {
      errorAppend = "\n\nRoVer's position in the role list is below that of this user. With certain setups, this will prevent RoVer from working correctly. Please have a server admin drag RoVer's role above all other roles in order to fix this problem."
    }

    try {
      // Read user data from memory, or request it if there isn't any cached.
      data = await Cache.get('users', this.id)
      if (!data) {
        data = await request({
          uri: `https://verify.eryn.io/api/user/${this.id}`,
          json: true,
          simple: false
        })
        freshData = true
      }
    } catch (e) {
      if (config.loud) console.log(e)
      return {
        status: false,
        error: 'Unknown error.'
      }
    }

    // If the status is ok, the user is in the database.
    if (data.status === 'ok') {
      // We only want to update the username if this data isn't from the cache.
      if (freshData) {
        // Resolve the Roblox username from the user id.
        let apiUserData = {}
        try {
          apiUserData = await request({
            uri: `http://api.roblox.com/users/${data.robloxId}`,
            json: true,
            simple: false
          })
        } catch (e) {
          return false
        }

        if (apiUserData.Username) {
          data.robloxUsername = apiUserData.Username
        }
      }

      // Cache the data for future use.
      Cache.set('users', this.id, data)

      // Check if these settings are enabled for this specific server,
      // if so, then put the member in the correct state.

      if (this.discordServer.getSetting('verifiedRole')) {
        let role = this.discordServer.getSetting('verifiedRole')
        if (!this.member.roles.get(role)) {
          try {
            await this.member.addRole(role)
          } catch (e) {
            if (config.loud) console.log(e)
            return {
              status: false,
              nonFatal: true,
              error: "RoVer doesn't have permissions to add roles to that user." + errorAppend
            }
          }
        }
      }

      if (this.discordServer.getSetting('verifiedRemovedRole')) {
        let role = this.discordServer.getSetting('verifiedRemovedRole')
        if (this.member.roles.get(role)) {
          try {
            await this.member.removeRole(role)
          } catch (e) {
            if (config.loud) console.log(e)
            return {
              status: false,
              nonFatal: true,
              error: "RoVer doesn't have permission to remove roles from that user." + errorAppend
            }
          }
        }
      }

      if (this.discordServer.getSetting('nicknameUsers')) {
        let nickname = this.getNickname(data).substring(0, 32)
        if (this.member.displayName !== nickname) {
          try {
            await this.member.setNickname(nickname)
          } catch (e) {
            if (config.loud) console.log(e)
            return {
              status: false,
              nonFatal: true,
              error: this.member.guild.ownerID === this.member.id ? "Sorry, RoVer can't change the server owner's nickname due to Discord permission restrictions. Please manually update your nickname. Or don't, I'm just an error message." : "RoVer doesn't have permission to change that user's nickname." + errorAppend
            }
          }
        }
      }

      if (this.discordServer.getSetting('announceChannel') && options.announce !== false) {
        let channel = await this.server.channels.get(this.discordServer.getSetting('announceChannel'))

        if (channel) {
          try {
            channel.send(`**User verified:** <@${this.id}> as ${data.robloxUsername}`)
          } catch (e) {}
        }
      }

      // Check if we want to resolve group rank bindings with cached or fresh data.
      if (options.clearBindingsCache !== false) {
        await Cache.clear(`bindings.${data.robloxId}`)
      }

      // Resolve group rank bindings for this member.
      if (this.discordServer.getSetting('groupRankBindings').length > 0) {
        for (let binding of this.discordServer.getSetting('groupRankBindings')) {
          // We use a Promise.then here so that they all execute asynchronously.
          DiscordServer.resolveGroupRankBinding(binding, data.robloxId, data.robloxUsername)
            .then((state) => {
              let hasRole = this.member.roles.get(binding.role) != null
              if (hasRole === state) return

              if (state === true) {
                this.member.addRole(binding.role).catch(e => {})
              } else {
                this.member.removeRole(binding.role).catch(e => {})
              }
            })
            .catch(e => {
              if (config.loud) console.log(e)
              console.log('Resolution error for binding')
            })
        }
      }

      return {
        status: true,
        robloxUsername: data.robloxUsername,
        robloxId: data.robloxId,
        discordId: this.member.id,
        discordName: this.member.user.username
      }
    } else {
      // Status was not "ok".
      switch (data.errorCode) {
        case 404:
        // User isn't in the database.

        // Add the "Not Verified" role to the user.
          if (this.discordServer.getSetting('verifiedRemovedRole')) {
            try {
              await this.member.addRole(this.discordServer.getSetting('verifiedRemovedRole'))
            } catch (e) {}
          }

          return {
            status: false,
            error: 'You don\'t seem to be verified! Please go to https://verify.eryn.io and follow the instructions on the page in order to get verified.'
          }
        case 429:
        // This client has exceeded the amount of requests allowed in a 60 second period.
          return {
            status: false,
            error: 'Server is busy. Please try again later.'
          }
        default:
        // Something else is wrong.
          return {
            status: false,
            error: 'Unknown error.'
          }
      }
    }
  }
}

module.exports = DiscordMember
<|endoftext|>


<commit_before>class BoardElement {
	constructor(board, boardPaper, Snap) {
		this.board = board;
		this.boardPaper = boardPaper;
		this.Snap = Snap;
		const {width, height} = this.board;
		const hCenter = ((width - 1) / 2 + 1) * 20;
		const vCenter = ((height - 1) / 2 + 1) * 20;
		this.boardEdge = this.boardPaper.rect(0, 0, (width + 1) * 20, (height + 1) * 20).addClass('board-edge');
		this.goalCircleA = this.boardPaper.circle(hCenter, (height + 1) * 20, 5).addClass('board-goal player-a');
		this.goalCircleB = this.boardPaper.circle(hCenter, 0, 5).addClass('board-goal player-b');
		this.traceLine = this.boardPaper.polyline(hCenter, vCenter).addClass('trace-line');
		this.currentPointCircle = this.boardPaper.circle(hCenter, vCenter, 7).addClass('current-point');
		this.updateSize();
		this.arrowMap = new Map();
		this.update();

		board.on('formedTriangle', (oldPoint, newPoint, thirdPoint) => {
			this.visualizeTriangle(oldPoint, newPoint, thirdPoint);
		});
		board.on('undo', () => {
			this.update();
		});
		board.on('win', (/* winner */) => {
			// something
		});
		board.on('switchPlayer', (/* newPlayer */) => {
			// something
		});
		board.on('moved', () => {
			this.update();
		});
		board.on('moving', () => {
			this.boardPaper.selectAll('.triangle').remove();
		});
		board.on('updateSize', () => {
			this.updateSize();
		});
	}

	updateSize() {
		const {width, height} = this.board;
		const hCenter = ((width - 1) / 2 + 1) * 20;
		this.boardPaper.attr({
			viewBox: [-3, -25, (width + 1) * 20 + 6, (height + 1) * 20 + 50],
		});
		this.boardEdge.attr({height: (height + 1) * 20, width: (width + 1) * 20});
		this.boardPaper.selectAll('.board-point').remove();
		for (let y = 0; y < height; y++) {
			for (let x = 0; x < width; x++) {
				this.boardPaper.circle(20 + 20 * x, 20 + 20 * y, 4).addClass('board-point');
			}
		}
		this.goalCircleA.attr({cx: hCenter, cy: (height + 1) * 20});
		this.goalCircleB.attr({cx: hCenter, cy: 0});

		// Bring to front
		this.boardPaper.append(this.traceLine);
		this.boardPaper.append(this.currentPointCircle);
	}

	update() {
		const player = ['a', 'b'][this.board.activePlayer];
		const currentPoint = this.board.getCurrentPoint();
		this.traceLine.attr({
			points: this.board.trace.map((path) => path.map(([x, y]) => [20 + 20 * x, 20 + 20 * y])),
		});
		const [x, y] = this.board.currentCoords;
		this.currentPointCircle.attr({
			cx: 20 + 20 * x,
			cy: 20 + 20 * y,
		});
		for (const arrow of this.arrowMap) {
			arrow.remove();
		}
		if (currentPoint !== null) {
			this.arrowMap = this.drawArrows(currentPoint);
			const isActive = this.board.isActive();
			if (isActive) {
				this.arrowMap.forEach((arrow, direction) => {
					arrow.click(() => {
						this.board.moveTo(direction);
					});
				});
			}
			this.boardPaper.paper.removeClass('active-a active-b inactive').addClass(`active-${player} ${
				isActive ? '' : 'inactive'
			}`);
		} else {
			this.arrowMap = new Map();
		}
	}

	drawArrows(point) {
		const arrowMap = new Map();
		for (const direction of point.movableDirections) {
			arrowMap.set(direction, this.drawArrow(point, direction));
		}
		return arrowMap;
	}

	drawArrow(point, direction) {
		const matrix = this.Snap.matrix();
		const {x, y} = point;
		const theta = direction * 45;
		matrix.translate(20 + x * 20, 20 + y * 20).rotate(theta);
		let pathData = null;
		if (direction % 2 === 0) {
			// N, E, S, W
			pathData = 'm -4,-13.085935 2,0 0,4.546875 4,0 0,-4.546875 2,0 -4,-4.375 z';
		} else {
			// NE, SE, SW, NW
			const pathDataUpRight = 'm 19.14693,-32.2842712474 0,2 -21.14693,0 0,21.74521 4,0 0,-17.74521 17.14693,0 0,2 4.375,-4 z';
			const pathDataUpRightDown = 'm 32.285156,-9.1367188 -2,0 0,-21.1484372 -32.285156,0 0,21.7460935 4,0 0,-17.7460935 24.285156,0 0,17.1484372 -2,0 4,4.375 z';
			const right = this.board.width - 1;
			const center = (this.board.width - 1) / 2;
			const bottom = this.board.height - 1;
			if (
				(direction === 1 && x === right - 1 && y === 0) ||
				(direction === 3 && x === right && y === bottom - 1) ||
				(direction === 5 && x === 1 && y === bottom) ||
				(direction === 7 && x === 0 && y === 1)
			) {
				pathData = pathDataUpRightDown;
			} else if (
				(direction === 1 && x === right && y === 1) ||
				(direction === 3 && x === right - 1 && y === bottom) ||
				(direction === 5 && x === 0 && y === bottom - 1) ||
				(direction === 7 && x === 1 && y === 0)
			) {
				pathData = pathDataUpRightDown;
				matrix.scale(-1, 1);
			} else if (
				(direction === 1 && x !== center - 1 && y === 0) ||
				(direction === 3 && x === right) ||
				(direction === 5 && x !== center + 1 && y === bottom) ||
				(direction === 7 && x === 0)
			) {
				pathData = pathDataUpRight;
			} else if (
				(direction === 1 && x === right) ||
				(direction === 3 && x !== center - 1 && y === bottom) ||
				(direction === 5 && x === 0) ||
				(direction === 7 && x !== center + 1 && y === 0)
			) {
				pathData = pathDataUpRight;
				matrix.scale(-1, 1);
			} else {
				pathData = 'm -4,-19 2,0 0,10.5 4,0 0,-10.5 2,0 -4,-4.4 z';
			}
		}
		return this.boardPaper.path(pathData).addClass('arrow').transform(matrix);
	}

	visualizeTriangle(point1, point2, point3) {
		this.boardPaper.selectAll('.triangle').remove();
		this.boardPaper.polygon([point1.x, point1.y, point2.x, point2.y, point3.x, point3.y].map((v) => 20 + 20 * v)).addClass('triangle');
	}

	startDrag() {
		this.selectedDirection = null;
		this.boardPaper.addClass('dragging');

		// Bring to front
		this.boardPaper.append(this.currentPointCircle);
	}

	dragTo(clientX, clientY) {
		const inverse = this.Snap.matrix(this.boardPaper.node.getScreenCTM().inverse());
		const svgX = inverse.x(clientX, clientY);
		const svgY = inverse.y(clientX, clientY);
		this.currentPointCircle.attr({
			cx: svgX,
			cy: svgY,
		});
		const [origX, origY] = this.board.currentCoords;
		const dx = svgX - (20 + 20 * origX);
		const dy = svgY - (20 + 20 * origY);
		for (const arrow of this.arrowMap) {
			arrow.removeClass('selected');
		}
		this.selectedDirection = null;
		if (dx * dx + dy * dy > 100) {
			// -PI <= theta <= PI
			const theta = Math.atan2(dy, dx);
			const selectedDirection = (Math.round(theta / (Math.PI / 4)) + 10) % 8;
			const arrow = this.arrowMap.get(selectedDirection);
			if (arrow) {
				arrow.addClass('selected');
				this.selectedDirection = selectedDirection;
			}
		}
	}

	endDrag() {
		this.boardPaper.removeClass('dragging');
		if (this.selectedDirection !== null) {
			this.board.moveTo(this.selectedDirection);
		} else {
			const [x, y] = this.board.currentCoords;
			this.currentPointCircle.attr({
				cx: 20 + 20 * x,
				cy: 20 + 20 * y,
			});
		}
	}
}

module.exports = BoardElement;
<commit_msg>Map() for-of iterates both key and values<commit_after>class BoardElement {
	constructor(board, boardPaper, Snap) {
		this.board = board;
		this.boardPaper = boardPaper;
		this.Snap = Snap;
		const {width, height} = this.board;
		const hCenter = ((width - 1) / 2 + 1) * 20;
		const vCenter = ((height - 1) / 2 + 1) * 20;
		this.boardEdge = this.boardPaper.rect(0, 0, (width + 1) * 20, (height + 1) * 20).addClass('board-edge');
		this.goalCircleA = this.boardPaper.circle(hCenter, (height + 1) * 20, 5).addClass('board-goal player-a');
		this.goalCircleB = this.boardPaper.circle(hCenter, 0, 5).addClass('board-goal player-b');
		this.traceLine = this.boardPaper.polyline(hCenter, vCenter).addClass('trace-line');
		this.currentPointCircle = this.boardPaper.circle(hCenter, vCenter, 7).addClass('current-point');
		this.updateSize();
		this.arrowMap = new Map();
		this.update();

		board.on('formedTriangle', (oldPoint, newPoint, thirdPoint) => {
			this.visualizeTriangle(oldPoint, newPoint, thirdPoint);
		});
		board.on('undo', () => {
			this.update();
		});
		board.on('win', (/* winner */) => {
			// something
		});
		board.on('switchPlayer', (/* newPlayer */) => {
			// something
		});
		board.on('moved', () => {
			this.update();
		});
		board.on('moving', () => {
			this.boardPaper.selectAll('.triangle').remove();
		});
		board.on('updateSize', () => {
			this.updateSize();
		});
	}

	updateSize() {
		const {width, height} = this.board;
		const hCenter = ((width - 1) / 2 + 1) * 20;
		this.boardPaper.attr({
			viewBox: [-3, -25, (width + 1) * 20 + 6, (height + 1) * 20 + 50],
		});
		this.boardEdge.attr({height: (height + 1) * 20, width: (width + 1) * 20});
		this.boardPaper.selectAll('.board-point').remove();
		for (let y = 0; y < height; y++) {
			for (let x = 0; x < width; x++) {
				this.boardPaper.circle(20 + 20 * x, 20 + 20 * y, 4).addClass('board-point');
			}
		}
		this.goalCircleA.attr({cx: hCenter, cy: (height + 1) * 20});
		this.goalCircleB.attr({cx: hCenter, cy: 0});

		// Bring to front
		this.boardPaper.append(this.traceLine);
		this.boardPaper.append(this.currentPointCircle);
	}

	update() {
		const player = ['a', 'b'][this.board.activePlayer];
		const currentPoint = this.board.getCurrentPoint();
		this.traceLine.attr({
			points: this.board.trace.map((path) => path.map(([x, y]) => [20 + 20 * x, 20 + 20 * y])),
		});
		const [x, y] = this.board.currentCoords;
		this.currentPointCircle.attr({
			cx: 20 + 20 * x,
			cy: 20 + 20 * y,
		});
		for (const arrow of this.arrowMap.values()) {
			arrow.remove();
		}
		if (currentPoint !== null) {
			this.arrowMap = this.drawArrows(currentPoint);
			const isActive = this.board.isActive();
			if (isActive) {
				this.arrowMap.forEach((arrow, direction) => {
					arrow.click(() => {
						this.board.moveTo(direction);
					});
				});
			}
			this.boardPaper.paper.removeClass('active-a active-b inactive').addClass(`active-${player} ${
				isActive ? '' : 'inactive'
			}`);
		} else {
			this.arrowMap = new Map();
		}
	}

	drawArrows(point) {
		const arrowMap = new Map();
		for (const direction of point.movableDirections) {
			arrowMap.set(direction, this.drawArrow(point, direction));
		}
		return arrowMap;
	}

	drawArrow(point, direction) {
		const matrix = this.Snap.matrix();
		const {x, y} = point;
		const theta = direction * 45;
		matrix.translate(20 + x * 20, 20 + y * 20).rotate(theta);
		let pathData = null;
		if (direction % 2 === 0) {
			// N, E, S, W
			pathData = 'm -4,-13.085935 2,0 0,4.546875 4,0 0,-4.546875 2,0 -4,-4.375 z';
		} else {
			// NE, SE, SW, NW
			const pathDataUpRight = 'm 19.14693,-32.2842712474 0,2 -21.14693,0 0,21.74521 4,0 0,-17.74521 17.14693,0 0,2 4.375,-4 z';
			const pathDataUpRightDown = 'm 32.285156,-9.1367188 -2,0 0,-21.1484372 -32.285156,0 0,21.7460935 4,0 0,-17.7460935 24.285156,0 0,17.1484372 -2,0 4,4.375 z';
			const right = this.board.width - 1;
			const center = (this.board.width - 1) / 2;
			const bottom = this.board.height - 1;
			if (
				(direction === 1 && x === right - 1 && y === 0) ||
				(direction === 3 && x === right && y === bottom - 1) ||
				(direction === 5 && x === 1 && y === bottom) ||
				(direction === 7 && x === 0 && y === 1)
			) {
				pathData = pathDataUpRightDown;
			} else if (
				(direction === 1 && x === right && y === 1) ||
				(direction === 3 && x === right - 1 && y === bottom) ||
				(direction === 5 && x === 0 && y === bottom - 1) ||
				(direction === 7 && x === 1 && y === 0)
			) {
				pathData = pathDataUpRightDown;
				matrix.scale(-1, 1);
			} else if (
				(direction === 1 && x !== center - 1 && y === 0) ||
				(direction === 3 && x === right) ||
				(direction === 5 && x !== center + 1 && y === bottom) ||
				(direction === 7 && x === 0)
			) {
				pathData = pathDataUpRight;
			} else if (
				(direction === 1 && x === right) ||
				(direction === 3 && x !== center - 1 && y === bottom) ||
				(direction === 5 && x === 0) ||
				(direction === 7 && x !== center + 1 && y === 0)
			) {
				pathData = pathDataUpRight;
				matrix.scale(-1, 1);
			} else {
				pathData = 'm -4,-19 2,0 0,10.5 4,0 0,-10.5 2,0 -4,-4.4 z';
			}
		}
		return this.boardPaper.path(pathData).addClass('arrow').transform(matrix);
	}

	visualizeTriangle(point1, point2, point3) {
		this.boardPaper.selectAll('.triangle').remove();
		this.boardPaper.polygon([point1.x, point1.y, point2.x, point2.y, point3.x, point3.y].map((v) => 20 + 20 * v)).addClass('triangle');
	}

	startDrag() {
		this.selectedDirection = null;
		this.boardPaper.addClass('dragging');

		// Bring to front
		this.boardPaper.append(this.currentPointCircle);
	}

	dragTo(clientX, clientY) {
		const inverse = this.Snap.matrix(this.boardPaper.node.getScreenCTM().inverse());
		const svgX = inverse.x(clientX, clientY);
		const svgY = inverse.y(clientX, clientY);
		this.currentPointCircle.attr({
			cx: svgX,
			cy: svgY,
		});
		const [origX, origY] = this.board.currentCoords;
		const dx = svgX - (20 + 20 * origX);
		const dy = svgY - (20 + 20 * origY);
		for (const arrow of this.arrowMap.values()) {
			arrow.removeClass('selected');
		}
		this.selectedDirection = null;
		if (dx * dx + dy * dy > 100) {
			// -PI <= theta <= PI
			const theta = Math.atan2(dy, dx);
			const selectedDirection = (Math.round(theta / (Math.PI / 4)) + 10) % 8;
			const arrow = this.arrowMap.get(selectedDirection);
			if (arrow) {
				arrow.addClass('selected');
				this.selectedDirection = selectedDirection;
			}
		}
	}

	endDrag() {
		this.boardPaper.removeClass('dragging');
		if (this.selectedDirection !== null) {
			this.board.moveTo(this.selectedDirection);
		} else {
			const [x, y] = this.board.currentCoords;
			this.currentPointCircle.attr({
				cx: 20 + 20 * x,
				cy: 20 + 20 * y,
			});
		}
	}
}

module.exports = BoardElement;
<|endoftext|>


<commit_before>// ----- Settings ----- //

// Camera settings.
var CAMERA_X = 0;
var CAMERA_Y = 3;
var CAMERA_Z = 7;
var CAMERA_FOV = 75;
var CAMERA_NEAR = 0.1;
var CAMERA_FAR = 1000;

// Dimensions.
var DISPLAY_WIDTH = window.innerWidth;
var DISPLAY_HEIGHT = window.innerHeight;
var PLANET_RADIUS = 1;

// ----- Functions ----- //

// Creates and returns the camera.
function setupCamera () {

	var aspectRatio = DISPLAY_WIDTH / DISPLAY_HEIGHT;
	var camera = new THREE.PerspectiveCamera(CAMERA_FOV, aspectRatio,
		CAMERA_NEAR, CAMERA_FAR);

	camera.position.set(CAMERA_X, CAMERA_Y, CAMERA_Z);

	return camera;

}

// Builds the renderer, adds it to the page, and returns it.
function setupRenderer () {

	var renderer = new THREE.WebGLRenderer();
	renderer.setSize(DISPLAY_WIDTH, DISPLAY_HEIGHT);

	document.body.appendChild(renderer.domElement);

	return renderer;

}

// Creates and returns a celestial body from a geometry and a material.
function celestialBody (geometry, meterial, position) {

	var pivot = new THREE.Object3D();
	var body = new THREE.Mesh(geometry, meterial);

	body.position.set.apply(this, position);
	pivot.add(body);

	return pivot;

}

function addPlanets (scene) {

	var geometry = new THREE.SphereGeometry(PLANET_RADIUS, 32, 32);
	var material = new THREE.MeshBasicMaterial({color: 0xffff00});

	var planetOne = celestialBody(geometry, material, [0, 0, 0]);
	scene.add(planetOne);

}

// Creates the render function and begins rendering.
function startRender (scene, camera, controls, renderer) {

	function render () {
		requestAnimationFrame(render);
		controls.update();
		renderer.render(scene, camera);
	}

	render();

}

// Sets up and launches the application.
function setup () {

	var scene = new THREE.Scene();
	var camera = setupCamera();
	var controls = new THREE.OrbitControls(camera);
	var renderer = setupRenderer();

	addPlanets(scene);

	startRender(scene, camera, controls, renderer);

}

// ----- DOM Loaded ----- //

document.addEventListener('DOMContentLoaded', setup);
<commit_msg>Added global object to hold properties of bodies. Changed addPlanets to addBodies function, now loops through this object and adds bodies. Updated celestialBody function to handle geometry and material.<commit_after>// ----- Settings ----- //

// Camera settings.
var CAMERA_X = 0;
var CAMERA_Y = 3;
var CAMERA_Z = 7;
var CAMERA_FOV = 75;
var CAMERA_NEAR = 0.1;
var CAMERA_FAR = 1000;

// Dimensions.
var DISPLAY_WIDTH = window.innerWidth;
var DISPLAY_HEIGHT = window.innerHeight;

// Planets.
var BODIES = {
	theSun: {
		colour: 0xffff00,
		position: [0, 0, 0],
		radius: 1
	}
};

// ----- Functions ----- //

// Creates and returns the camera.
function setupCamera () {

	var aspectRatio = DISPLAY_WIDTH / DISPLAY_HEIGHT;
	var camera = new THREE.PerspectiveCamera(CAMERA_FOV, aspectRatio,
		CAMERA_NEAR, CAMERA_FAR);

	camera.position.set(CAMERA_X, CAMERA_Y, CAMERA_Z);

	return camera;

}

// Builds the renderer, adds it to the page, and returns it.
function setupRenderer () {

	var renderer = new THREE.WebGLRenderer();
	renderer.setSize(DISPLAY_WIDTH, DISPLAY_HEIGHT);

	document.body.appendChild(renderer.domElement);

	return renderer;

}

// Creates and returns a celestial body from a material and its properties.
function celestialBody (info) {

	var geometry = new THREE.SphereGeometry(info.radius, 32, 32);
	var material = new THREE.MeshBasicMaterial({color: info.colour});

	var pivot = new THREE.Object3D();
	var body = new THREE.Mesh(geometry, material);

	body.position.set.apply(this, info.position);
	pivot.add(body);

	return pivot;

}

// Adds celestial bodies to the scene.
function addBodies (scene) {

	for (var body in BODIES) {

		var newBody = celestialBody(BODIES[body]);
		scene.add(newBody);

	}

}

// Creates the render function and begins rendering.
function startRender (scene, camera, controls, renderer) {

	function render () {
		requestAnimationFrame(render);
		controls.update();
		renderer.render(scene, camera);
	}

	render();

}

// Sets up and launches the application.
function setup () {

	var scene = new THREE.Scene();
	var camera = setupCamera();
	var controls = new THREE.OrbitControls(camera);
	var renderer = setupRenderer();

	addBodies(scene);

	startRender(scene, camera, controls, renderer);

}

// ----- DOM Loaded ----- //

document.addEventListener('DOMContentLoaded', setup);
<|endoftext|>