docs: function(options) {
      var buttonInfos = [];
      var buttons = options.buttons;
      //var expirationTime = 2000;  // 2 seconds, 2000ms
  
      // I don't really know what to make this number
      // If the person has steady hands they can make
      // this fail but I'm going to assume most players
      // most of the time won't hold steady for this long.
      // On the other hand if the button does get stuck
      // It will take this long to un-stick.
  
      for (var ii = 0; ii < buttons.length; ++ii) {
        var button = buttons[ii];
        var buttonInfo = {
          pointerIds: {},   // Pointers currently in this button
          numPointerIds: 0, // Number of pointers in this button
        };
        Misc.copyProperties(button, buttonInfo);
        buttonInfos.push(buttonInfo);
      }
  
      // var printButtonInfo = function(buttonInfo) {
      //   console.log("button: " + buttonInfo.element.id + ", " + buttonInfo.numPointerIds);
      // };
  
      var addPointerId = function(buttonInfo, pointerId, timeStamp) {
        if (!buttonInfo.pointerIds[pointerId]) {
          buttonInfo.pointerIds[pointerId] = timeStamp;
          ++buttonInfo.numPointerIds;
          buttonInfo.callback({pressed: true});
        }
      };
  
      var removePointerId = function(buttonInfo, pointerId) {
        if (buttonInfo.pointerIds[pointerId]) {
          delete buttonInfo.pointerIds[pointerId];
          --buttonInfo.numPointerIds;
          if (buttonInfo.numPointerIds === 0) {
            buttonInfo.callback({pressed: false});
          } else if (buttonInfo.numPointerIds < 0) {
            throw ("numPointerIds went negative: how did I get here!?");
          }
        }
      };
  
      // This is because (maybe because my programming sucks?)
      // sometimes it seems we miss an out/up event and buttons
      // get stuck. So, for a particlar id, if no event has come in
      // for a while assume it was released.
      //var expireOldButtons = function() {
      //  var now = Date.now();
      //  buttonInfos.forEach(function(buttonInfo) {
      //    Object.keys(buttonInfo.pointerIds).forEach(function(pointerId) {
      //      var timeStamp = buttonInfo.pointerIds[pointerId];
      //      var age = now - timeStamp;
      //      if (age > expirationTime) {
      //        removePointerId(buttonInfo, pointerId);
      //      }
      //    });
      //  });
      //};
  
      var handleButtonDown = function(e, buttonInfo) {
        addPointerId(buttonInfo, e.pointerId, e.timeStamp);
      };
  
      var handleButtonUp = function(e, buttonInfo) {
        removePointerId(buttonInfo, e.pointerId, e.timeStamp);
      };
  
      var handleButtonMove = function(/*e, buttonInfo*/) {
  //      addPointerId(buttonInfo, e.pointerId, e.timeStamp);
      };
  
      var handleButtonOut = function(e, buttonInfo) {
        removePointerId(buttonInfo, e.pointerId, e.timeStamp);
      };
  
      var handleButtonEnter = function(e, buttonInfo) {
        addPointerId(buttonInfo, e.pointerId, e.timeStamp);
      };
  
      var handleButtonLeave = function(e, buttonInfo) {
        removePointerId(buttonInfo, e.pointerId, e.timeStamp);
      };
  
      var handleButtonCancel = function(e, buttonInfo) {
        removePointerId(buttonInfo, e.pointerId, e.timeStamp);
      };
  
      var funcs = {
        pointerdown: handleButtonDown,
        pointermove: handleButtonMove,
        pointerup: handleButtonUp,
        pointerout: handleButtonOut,
        pointerenter: handleButtonEnter,
        pointerleave: handleButtonLeave,
        pointercancel: handleButtonCancel,
      };
  
      buttonInfos.forEach(function(buttonInfo) {
        var elem = buttonInfo.element;
        Object.keys(funcs).forEach(function(eventName) {
          var func = funcs[eventName];
          elem.addEventListener(eventName, function(buttonInfo) {
            return function(e) {
              func(e, buttonInfo);
            };
          }(buttonInfo));
        });
      });
  
  //    setInterval(expireOldButtons, 100);
    }