docs: function(options) {
      var callback = options.callback;
      var container = options.inputElement;
      options.deadSpaceRadius = options.deadSpaceRadius || 10;
      var deadSpaceRadiusSq = options.deadSpaceRadius * options.deadSpaceRadius;
  
      var Vector2 = function(x, y) {
        this.reset(x, y);
      };
  
      Vector2.prototype.reset = function(x, y) {
        this.x = x;
        this.y = y;
        return this;
      };
  
      Vector2.prototype.copyFrom = function(src) {
        this.x = src.x;
        this.y = src.y;
      };
  
      Vector2.prototype.minusEq = function(v) {
        this.x -= v.x;
        this.y -= v.y;
        return this;
      };
  
      var makePad = function(padId) {
        return {
          pointerId: -1,                      // touch id
          pointerPos: new Vector2(0, 0),      // current position
          pointerStartPos: new Vector2(0, 0), // position when first touched
          vector: new Vector2(0, 0),          // vector from start to current position
          dir: -1,                            // octant
          lastDir: 0,                         // previous octant
          event: Input.createDirectionEventInfo(padId),
        };
      };
  
      var pads = [];
      for (var ii = 0; ii < options.pads.length; ++ii) {
        pads.push(makePad(ii));
      }
  
      var computeDirByAngle = function(x, y) {
        var angle = Math.atan2(-y, x) + Math.PI * 2 + Math.PI / 8;
        return (Math.floor(angle / (Math.PI / 4))) % 8;
      };
  
      var computeDirByAngle4 = function(x, y) {
        if (Math.abs(x) < Math.abs(y)) {
          return y < 0 ? 2 : 6;
        } else {
          return x < 0 ? 4 : 0;
        }
      };
  
      //      |   |
      //      | V |x
      // -----+---+-----
      //  H   |HV |x H
      // -----+---+-----
      //  y   | Vy|xy
      //      |   |
  
      var axisBitsToDir = [
         3, // 0
         4, // 1   h
         2, // 2    v
        -1, // 3   hv
         1, // 4     x
         0, // 5   h x
         2, // 6    vx
        -1, // 7   hvx
         5, // 8      y
         4, // 9   h  y
         6, // 10   v y
        -1, // 11  hv y
         7, // 12    xy
         0, // 13  h xy
         6, // 14   vxy
        -1, // 15  hvxy
      ];
  
      var computeDirByAxis = function(x, y) {
        var h = (Math.abs(y) < options.axisSize / 2) ? 1 : 0;
        var v = (Math.abs(x) < options.axisSize / 2) ? 2 : 0;
        var bits = h | v |
            (x > 0 ? 4 : 0) |
            (y > 0 ? 8 : 0);
        return axisBitsToDir[bits];
      };
  
      var computeDir = options.axisSize ? computeDirByAxis : computeDirByAngle;
  
      if (options.divisions === 4) {
        computeDir = computeDirByAngle4;
      }
  
      var callCallback = function(padId, dir) {
        var pad = pads[padId];
        Input.emitDirectionEvent(padId, dir, pad.event, callback);
      };
  
      var updatePad = function(pad, padId, out) {
        var newDir = -1;
        if (!out && pad.pointerId >= 0) {
          var distSq = pad.vector.x * pad.vector.x + pad.vector.y * pad.vector.y;
          if (distSq > deadSpaceRadiusSq) {
            newDir = computeDir(pad.vector.x, pad.vector.y);
            pad.lastDir = newDir;
          }
        }
        if (pad.dir !== newDir) {
          pad.dir = newDir;
          callCallback(padId, newDir);
        }
      };
  
      var checkStart = function(padId, e) {
        var pad = pads[padId];
        var padOptions = options.pads[padId];
        pad.pointerId = e.pointerId;
        var relPos = Input.getRelativeCoordinates(padOptions.referenceElement, e);
        var x = relPos.x - (padOptions.offsetX || padOptions.referenceElement.clientWidth  / 2);
        var y = relPos.y - (padOptions.offsetY || padOptions.referenceElement.clientHeight / 2);
        if (options.fixedCenter) {
          pad.pointerStartPos.reset(0, 0);
          pad.pointerPos.reset(x, y);
          pad.vector.reset(x, y);
          updatePad(pad, padId);
        } else {
          pad.pointerStartPos.reset(x, y);
          pad.pointerPos.copyFrom(pad.pointerStartPos);
          pad.vector.reset(0, 0);
          pad.dir = pad.lastDir;
          callCallback(padId, pad.lastDir);
        }
      };
  
      var getClosestPad = function(e) {
        var closestId = 0;
        var closestDist;
        for (var ii = 0; ii < pads.length; ++ii) {
          var padOptions = options.pads[ii];
          var refElement = padOptions.referenceElement;
          var relPos = Input.getRelativeCoordinates(refElement, e);
          var centerX = refElement.clientWidth / 2;
          var centerY = refElement.clientHeight / 2;
          var dx = relPos.x - centerX;
          var dy = relPos.y - centerY;
          var distSq = dx * dx + dy * dy;
          if (closestDist === undefined || distSq < closestDist) {
            closestDist = distSq;
            closestId = ii;
          }
        }
        return closestId;
      };
  
      var onPointerDown = function(e) {
        var padId = getClosestPad(e);
        checkStart(padId, e);
      };
  
      var onPointerMove = function(e) {
        for (var ii = 0; ii < pads.length; ++ii) {
          var pad = pads[ii];
          if (pad.pointerId === e.pointerId) {
            var padOptions = options.pads[ii];
            var relPos = Input.getRelativeCoordinates(padOptions.referenceElement, e);
            var x = relPos.x - (padOptions.offsetX || padOptions.referenceElement.clientWidth / 2);
            var y = relPos.y - (padOptions.offsetY || padOptions.referenceElement.clientHeight / 2);
            pad.pointerPos.reset(x, y);
            pad.vector.copyFrom(pad.pointerPos);
            pad.vector.minusEq(pad.pointerStartPos);
            updatePad(pad, ii);
          }
        }
      };
  
      var onPointerUp = function(e) {
        for (var ii = 0; ii < pads.length; ++ii) {
          var pad = pads[ii];
          if (pad.pointerId === e.pointerId) {
            pad.pointerId = -1;
            pad.vector.reset(0, 0);
            updatePad(pad, ii);
          }
        }
      };
  
      var onPointerOut = function(e) {
        for (var ii = 0; ii < pads.length; ++ii) {
          var pad = pads[ii];
          if (pad.pointerId === e.pointerId) {
            updatePad(pad, ii, true);
          }
        }
      };
  
      container.addEventListener('pointerdown', onPointerDown, false);
      container.addEventListener('pointermove', onPointerMove, false);
      container.addEventListener('pointerup', onPointerUp, false);
      container.addEventListener('pointerout', onPointerOut, false);
    }