<commit_before>(function () {
  var ns = $.namespace('pskl.controller.preview');

  // Preview is a square of PREVIEW_SIZE x PREVIEW_SIZE
  var PREVIEW_SIZE = 200;
  var RENDER_MINIMUM_DELAY = 300;

  ns.PreviewController = function (piskelController, container) {
    this.piskelController = piskelController;
    this.container = container;

    this.elapsedTime = 0;
    this.currentIndex = 0;

    this.onionSkinShortcut = pskl.service.keyboard.Shortcuts.MISC.ONION_SKIN;

    this.lastRenderTime = 0;
    this.renderFlag = true;

    this.fpsRangeInput = document.querySelector('#preview-fps');
    this.fpsCounterDisplay = document.querySelector('#display-fps');
    this.openPopupPreview = document.querySelector('.open-popup-preview-button');
    this.previewSizeDropdown = document.querySelector('.preview-drop-down');
    this.previewSizes = {
      original: {
        button: document.querySelector('.original-size-button'),
        shortcut: pskl.service.keyboard.Shortcuts.MISC.X1_PREVIEW,
        tooltip: 'Original size preview'
      },
      best: {
        button: document.querySelector('.best-size-button'),
        shortcut: pskl.service.keyboard.Shortcuts.MISC.BEST_PREVIEW,
        tooltip: 'Best size preview'
      },
      full: {
        button: document.querySelector('.full-size-button'),
        shortcut: pskl.service.keyboard.Shortcuts.MISC.FULL_PREVIEW,
        tooltip: 'Full size preview'
      }
    };
    this.toggleOnionSkinButton = document.querySelector('.preview-toggle-onion-skin');

    this.renderer = new pskl.rendering.frame.BackgroundImageFrameRenderer(this.container);
    this.popupPreviewController = new ns.PopupPreviewController(piskelController);
  };

  ns.PreviewController.prototype.init = function () {
    this.fpsRangeInput.addEventListener('change', this.onFpsRangeInputUpdate_.bind(this));
    this.fpsRangeInput.addEventListener('input', this.onFpsRangeInputUpdate_.bind(this));

    document.querySelector('.right-column').style.width = Constants.ANIMATED_PREVIEW_WIDTH + 'px';

    var addEvent = pskl.utils.Event.addEventListener;
    addEvent(this.toggleOnionSkinButton, 'click', this.toggleOnionSkin_, this);
    addEvent(this.openPopupPreview, 'click', this.onOpenPopupPreviewClick_, this);

    var registerShortcut = pskl.app.shortcutService.registerShortcut.bind(pskl.app.shortcutService);
    registerShortcut(this.onionSkinShortcut, this.toggleOnionSkin_.bind(this));

    var onionSkinTooltip = pskl.utils.TooltipFormatter.format('Toggle onion skin', this.onionSkinShortcut);
    this.toggleOnionSkinButton.setAttribute('title', onionSkinTooltip);

    for (var size in this.previewSizes) {
      if (this.previewSizes.hasOwnProperty(size)) {
        var previewSize = this.previewSizes[size];
        addEvent(previewSize.button, 'click', this.onChangePreviewSize_, this, size);
        registerShortcut(previewSize.shortcut, this.onChangePreviewSize_.bind(this, size));
        var tooltip = pskl.utils.TooltipFormatter.format(previewSize.tooltip, previewSize.shortcut);
        previewSize.button.setAttribute('title', tooltip);
      }
    }

    $.subscribe(Events.FRAME_SIZE_CHANGED, this.onFrameSizeChange_.bind(this));
    $.subscribe(Events.USER_SETTINGS_CHANGED, $.proxy(this.onUserSettingsChange_, this));
    $.subscribe(Events.PISKEL_SAVE_STATE, this.setRenderFlag_.bind(this, true));
    $.subscribe(Events.FPS_CHANGED, this.updateFPS_.bind(this));
    // On PISKEL_RESET, set the render flag and update the FPS input
    $.subscribe(Events.PISKEL_RESET, this.setRenderFlag_.bind(this, true));
    $.subscribe(Events.PISKEL_RESET, this.updateFPS_.bind(this));

    this.updatePreviewSizeButtons_();
    this.popupPreviewController.init();

    this.updateZoom_();
    this.updateOnionSkinPreview_();
    this.selectPreviewSizeButton_();
    this.updateFPS_();
    this.updateMaxFPS_();
    this.updateContainerDimensions_();
  };

  ns.PreviewController.prototype.updatePreviewSizeButtons_ = function () {
    var fullZoom = this.calculateZoom_();
    var bestZoom = Math.floor(fullZoom);
    var seamlessModeEnabled = pskl.UserSettings.get(pskl.UserSettings.SEAMLESS_MODE);

    var validSizes;
    if (fullZoom < 1) {
      this.disablePreviewSizeWidget_('No other option available');
      validSizes = ['full'];
    } else if (fullZoom === 1) {
      this.disablePreviewSizeWidget_('No other option available');
      validSizes = ['original'];
    } else if (seamlessModeEnabled) {
      this.disablePreviewSizeWidget_('Disabled in seamless mode');
      validSizes = ['original'];
    } else {
      this.enablePreviewSizeWidget_();
      if (fullZoom === bestZoom) {
        // If the full zoom is the same as the best zoom, display the best option only as
        // it gives the exact factor information.
        validSizes = ['original', 'best'];
      } else if (bestZoom === 1) {
        // If best zoom is 1x, remove it as it is redundant with the original option.
        validSizes = ['full', 'original'];
      } else {
        validSizes = ['full', 'original', 'best'];
      }
    }

    // Update buttons content and status.
    this.previewSizes.best.button.textContent = Math.floor(fullZoom) + 'x';
    for (var size in this.previewSizes) {
      if (this.previewSizes.hasOwnProperty(size)) {
        var previewSize = this.previewSizes[size];
        var isSizeEnabled = validSizes.indexOf(size) != -1;

        // classList.toggle is not available on IE11.
        if (isSizeEnabled) {
          previewSize.button.classList.remove('preview-contextual-action-hidden');
        } else {
          previewSize.button.classList.add('preview-contextual-action-hidden');
        }
      }
    }

    // Update the selected preview size if the currently selected size is not valid.
    var selectedSize = pskl.UserSettings.get(pskl.UserSettings.PREVIEW_SIZE);
    if (validSizes.indexOf(selectedSize) === -1) {
      this.onChangePreviewSize_(validSizes[0]);
    }
  };

  ns.PreviewController.prototype.enablePreviewSizeWidget_ = function () {
    this.previewSizeDropdown.classList.remove('preview-drop-down-disabled');
  };

  ns.PreviewController.prototype.disablePreviewSizeWidget_ = function (reason) {
    // The .preview-disable-overlay is displayed on top of the preview size widget
    document.querySelector('.preview-disable-overlay').setAttribute('data-original-title', reason);
    this.previewSizeDropdown.classList.add('preview-drop-down-disabled');
  };

  ns.PreviewController.prototype.onOpenPopupPreviewClick_ = function () {
    this.popupPreviewController.open();
  };

  ns.PreviewController.prototype.onChangePreviewSize_ = function (size) {
    var previewSize = this.previewSizes[size];
    var isEnabled = !previewSize.button.classList.contains('preview-contextual-action-hidden');
    if (isEnabled) {
      pskl.UserSettings.set(pskl.UserSettings.PREVIEW_SIZE, size);
    }
  };

  ns.PreviewController.prototype.onUserSettingsChange_ = function (evt, name, value) {
    if (name == pskl.UserSettings.ONION_SKIN) {
      this.updateOnionSkinPreview_();
    } else if (name == pskl.UserSettings.MAX_FPS) {
      this.updateMaxFPS_();
    } else if (name === pskl.UserSettings.SEAMLESS_MODE) {
      this.onFrameSizeChange_();
    } else {
      this.updateZoom_();
      this.selectPreviewSizeButton_();
      this.updateContainerDimensions_();
    }
  };

  ns.PreviewController.prototype.updateOnionSkinPreview_ = function () {
    var enabledClassname = 'preview-toggle-onion-skin-enabled';
    var isEnabled = pskl.UserSettings.get(pskl.UserSettings.ONION_SKIN);

    // classList.toggle is not available on IE11.
    if (isEnabled) {
      this.toggleOnionSkinButton.classList.add(enabledClassname);
    } else {
      this.toggleOnionSkinButton.classList.remove(enabledClassname);
    }
  };

  ns.PreviewController.prototype.selectPreviewSizeButton_ = function () {
    var currentlySelected = document.querySelector('.size-button-selected');
    if (currentlySelected) {
      currentlySelected.classList.remove('size-button-selected');
    }

    var selectedSize = pskl.UserSettings.get(pskl.UserSettings.PREVIEW_SIZE);
    var previewSize = this.previewSizes[selectedSize];
    previewSize.button.classList.add('size-button-selected');
  };

  ns.PreviewController.prototype.updateMaxFPS_ = function () {
    var maxFps = pskl.UserSettings.get(pskl.UserSettings.MAX_FPS);
    this.fpsRangeInput.setAttribute('max', maxFps);
    this.piskelController.setFPS(Math.min(maxFps, this.piskelController.getFPS()));
  };

  ns.PreviewController.prototype.updateZoom_ = function () {
    var previewSize = pskl.UserSettings.get(pskl.UserSettings.PREVIEW_SIZE);

    var zoom;
    if (previewSize === 'original') {
      zoom = 1;
    } else if (previewSize === 'best') {
      zoom = Math.floor(this.calculateZoom_());
    } else if (previewSize === 'full') {
      zoom = this.calculateZoom_();
    }

    this.renderer.setZoom(zoom);
    this.setRenderFlag_(true);
  };

  ns.PreviewController.prototype.getZoom = function () {
    return this.calculateZoom_();
  };

  ns.PreviewController.prototype.getCoordinates = function(x, y) {
    var containerOffset = this.container.offset();
    x = x - containerOffset.left;
    y = y - containerOffset.top;
    var zoom = this.getZoom();
    return {
      x : Math.floor(x / zoom),
      y : Math.floor(y / zoom)
    };
  };

  /**
   * Event handler triggered on 'input' or 'change' events.
   */
  ns.PreviewController.prototype.onFpsRangeInputUpdate_ = function (evt) {
    var fps = parseInt(this.fpsRangeInput.value, 10);
    this.piskelController.setFPS(fps);
    // blur only on 'change' events, as blurring on 'input' breaks on Firefox
    if (evt.type === 'change') {
      this.fpsRangeInput.blur();
    }
  };

  ns.PreviewController.prototype.updateFPS_ = function () {
    var fps = this.piskelController.getFPS();
    if (fps !== this.fps) {
      this.fps = fps;
      // reset
      this.fpsRangeInput.value = 0;
      // set proper value
      this.fpsRangeInput.value = this.fps;
      this.fpsCounterDisplay.innerHTML = this.fps + ' FPS';
    }
  };

  ns.PreviewController.prototype.render = function (delta) {
    this.elapsedTime += delta;
    var index = this.getNextIndex_(delta);
    if (this.shouldRender_() || this.currentIndex != index) {
      this.currentIndex = index;
      var frame = pskl.utils.LayerUtils.mergeFrameAt(this.piskelController.getLayers(), index);
      this.renderer.render(frame);
      this.renderFlag = false;
      this.lastRenderTime = Date.now();

      this.popupPreviewController.render(frame);
    }
  };

  ns.PreviewController.prototype.getNextIndex_ = function (delta) {
    if (this.fps === 0) {
      return this.piskelController.getCurrentFrameIndex();
    } else {
      var index = Math.floor(this.elapsedTime / (1000 / this.fps));
      if (!this.piskelController.hasFrameAt(index)) {
        this.elapsedTime = 0;
        index = 0;
      }
      return index;
    }
  };

  /**
   * Calculate the preview zoom depending on the framesheet size
   */
  ns.PreviewController.prototype.calculateZoom_ = function () {
    var frame = this.piskelController.getCurrentFrame();
    var hZoom = PREVIEW_SIZE / frame.getHeight();
    var wZoom = PREVIEW_SIZE / frame.getWidth();

    return Math.min(hZoom, wZoom);
  };

  ns.PreviewController.prototype.onFrameSizeChange_ = function () {
    this.updateZoom_();
    this.updateContainerDimensions_();
    this.updatePreviewSizeButtons_();
  };

  ns.PreviewController.prototype.updateContainerDimensions_ = function () {
    var isSeamless = pskl.UserSettings.get(pskl.UserSettings.SEAMLESS_MODE);
    this.renderer.setRepeated(isSeamless);

    var height, width;

    if (isSeamless) {
      height = PREVIEW_SIZE;
      width = PREVIEW_SIZE;
    } else {
      var zoom = this.getZoom();
      var frame = this.piskelController.getCurrentFrame();
      height = frame.getHeight() * zoom;
      width = frame.getWidth() * zoom;
    }

    var containerEl = this.container.get(0);
    containerEl.style.height = height + 'px';
    containerEl.style.width = width + 'px';

    var horizontalMargin = (PREVIEW_SIZE - height) / 2;
    containerEl.style.marginTop = horizontalMargin + 'px';
    containerEl.style.marginBottom = horizontalMargin + 'px';

    var verticalMargin = (PREVIEW_SIZE - width) / 2;
    containerEl.style.marginLeft = verticalMargin + 'px';
    containerEl.style.marginRight = verticalMargin + 'px';
  };

  ns.PreviewController.prototype.setRenderFlag_ = function (bool) {
    this.renderFlag = bool;
  };

  ns.PreviewController.prototype.shouldRender_ = function () {
    return (this.renderFlag || this.popupPreviewController.renderFlag) &&
            (Date.now() - this.lastRenderTime > RENDER_MINIMUM_DELAY);
  };

  ns.PreviewController.prototype.toggleOnionSkin_ = function () {
    var currentValue = pskl.UserSettings.get(pskl.UserSettings.ONION_SKIN);
    pskl.UserSettings.set(pskl.UserSettings.ONION_SKIN, !currentValue);
  };
})();
<commit_msg>Remove reference to seamless in tooltip<commit_after>(function () {
  var ns = $.namespace('pskl.controller.preview');

  // Preview is a square of PREVIEW_SIZE x PREVIEW_SIZE
  var PREVIEW_SIZE = 200;
  var RENDER_MINIMUM_DELAY = 300;

  ns.PreviewController = function (piskelController, container) {
    this.piskelController = piskelController;
    this.container = container;

    this.elapsedTime = 0;
    this.currentIndex = 0;

    this.onionSkinShortcut = pskl.service.keyboard.Shortcuts.MISC.ONION_SKIN;

    this.lastRenderTime = 0;
    this.renderFlag = true;

    this.fpsRangeInput = document.querySelector('#preview-fps');
    this.fpsCounterDisplay = document.querySelector('#display-fps');
    this.openPopupPreview = document.querySelector('.open-popup-preview-button');
    this.previewSizeDropdown = document.querySelector('.preview-drop-down');
    this.previewSizes = {
      original: {
        button: document.querySelector('.original-size-button'),
        shortcut: pskl.service.keyboard.Shortcuts.MISC.X1_PREVIEW,
        tooltip: 'Original size preview'
      },
      best: {
        button: document.querySelector('.best-size-button'),
        shortcut: pskl.service.keyboard.Shortcuts.MISC.BEST_PREVIEW,
        tooltip: 'Best size preview'
      },
      full: {
        button: document.querySelector('.full-size-button'),
        shortcut: pskl.service.keyboard.Shortcuts.MISC.FULL_PREVIEW,
        tooltip: 'Full size preview'
      }
    };
    this.toggleOnionSkinButton = document.querySelector('.preview-toggle-onion-skin');

    this.renderer = new pskl.rendering.frame.BackgroundImageFrameRenderer(this.container);
    this.popupPreviewController = new ns.PopupPreviewController(piskelController);
  };

  ns.PreviewController.prototype.init = function () {
    this.fpsRangeInput.addEventListener('change', this.onFpsRangeInputUpdate_.bind(this));
    this.fpsRangeInput.addEventListener('input', this.onFpsRangeInputUpdate_.bind(this));

    document.querySelector('.right-column').style.width = Constants.ANIMATED_PREVIEW_WIDTH + 'px';

    var addEvent = pskl.utils.Event.addEventListener;
    addEvent(this.toggleOnionSkinButton, 'click', this.toggleOnionSkin_, this);
    addEvent(this.openPopupPreview, 'click', this.onOpenPopupPreviewClick_, this);

    var registerShortcut = pskl.app.shortcutService.registerShortcut.bind(pskl.app.shortcutService);
    registerShortcut(this.onionSkinShortcut, this.toggleOnionSkin_.bind(this));

    var onionSkinTooltip = pskl.utils.TooltipFormatter.format('Toggle onion skin', this.onionSkinShortcut);
    this.toggleOnionSkinButton.setAttribute('title', onionSkinTooltip);

    for (var size in this.previewSizes) {
      if (this.previewSizes.hasOwnProperty(size)) {
        var previewSize = this.previewSizes[size];
        addEvent(previewSize.button, 'click', this.onChangePreviewSize_, this, size);
        registerShortcut(previewSize.shortcut, this.onChangePreviewSize_.bind(this, size));
        var tooltip = pskl.utils.TooltipFormatter.format(previewSize.tooltip, previewSize.shortcut);
        previewSize.button.setAttribute('title', tooltip);
      }
    }

    $.subscribe(Events.FRAME_SIZE_CHANGED, this.onFrameSizeChange_.bind(this));
    $.subscribe(Events.USER_SETTINGS_CHANGED, $.proxy(this.onUserSettingsChange_, this));
    $.subscribe(Events.PISKEL_SAVE_STATE, this.setRenderFlag_.bind(this, true));
    $.subscribe(Events.FPS_CHANGED, this.updateFPS_.bind(this));
    // On PISKEL_RESET, set the render flag and update the FPS input
    $.subscribe(Events.PISKEL_RESET, this.setRenderFlag_.bind(this, true));
    $.subscribe(Events.PISKEL_RESET, this.updateFPS_.bind(this));

    this.updatePreviewSizeButtons_();
    this.popupPreviewController.init();

    this.updateZoom_();
    this.updateOnionSkinPreview_();
    this.selectPreviewSizeButton_();
    this.updateFPS_();
    this.updateMaxFPS_();
    this.updateContainerDimensions_();
  };

  ns.PreviewController.prototype.updatePreviewSizeButtons_ = function () {
    var fullZoom = this.calculateZoom_();
    var bestZoom = Math.floor(fullZoom);
    var seamlessModeEnabled = pskl.UserSettings.get(pskl.UserSettings.SEAMLESS_MODE);

    var validSizes;
    if (fullZoom < 1) {
      this.disablePreviewSizeWidget_('No other option available');
      validSizes = ['full'];
    } else if (fullZoom === 1) {
      this.disablePreviewSizeWidget_('No other option available');
      validSizes = ['original'];
    } else if (seamlessModeEnabled) {
      this.disablePreviewSizeWidget_('Disabled in tile mode');
      validSizes = ['original'];
    } else {
      this.enablePreviewSizeWidget_();
      if (fullZoom === bestZoom) {
        // If the full zoom is the same as the best zoom, display the best option only as
        // it gives the exact factor information.
        validSizes = ['original', 'best'];
      } else if (bestZoom === 1) {
        // If best zoom is 1x, remove it as it is redundant with the original option.
        validSizes = ['full', 'original'];
      } else {
        validSizes = ['full', 'original', 'best'];
      }
    }

    // Update buttons content and status.
    this.previewSizes.best.button.textContent = Math.floor(fullZoom) + 'x';
    for (var size in this.previewSizes) {
      if (this.previewSizes.hasOwnProperty(size)) {
        var previewSize = this.previewSizes[size];
        var isSizeEnabled = validSizes.indexOf(size) != -1;

        // classList.toggle is not available on IE11.
        if (isSizeEnabled) {
          previewSize.button.classList.remove('preview-contextual-action-hidden');
        } else {
          previewSize.button.classList.add('preview-contextual-action-hidden');
        }
      }
    }

    // Update the selected preview size if the currently selected size is not valid.
    var selectedSize = pskl.UserSettings.get(pskl.UserSettings.PREVIEW_SIZE);
    if (validSizes.indexOf(selectedSize) === -1) {
      this.onChangePreviewSize_(validSizes[0]);
    }
  };

  ns.PreviewController.prototype.enablePreviewSizeWidget_ = function () {
    this.previewSizeDropdown.classList.remove('preview-drop-down-disabled');
  };

  ns.PreviewController.prototype.disablePreviewSizeWidget_ = function (reason) {
    // The .preview-disable-overlay is displayed on top of the preview size widget
    document.querySelector('.preview-disable-overlay').setAttribute('data-original-title', reason);
    this.previewSizeDropdown.classList.add('preview-drop-down-disabled');
  };

  ns.PreviewController.prototype.onOpenPopupPreviewClick_ = function () {
    this.popupPreviewController.open();
  };

  ns.PreviewController.prototype.onChangePreviewSize_ = function (size) {
    var previewSize = this.previewSizes[size];
    var isEnabled = !previewSize.button.classList.contains('preview-contextual-action-hidden');
    if (isEnabled) {
      pskl.UserSettings.set(pskl.UserSettings.PREVIEW_SIZE, size);
    }
  };

  ns.PreviewController.prototype.onUserSettingsChange_ = function (evt, name, value) {
    if (name == pskl.UserSettings.ONION_SKIN) {
      this.updateOnionSkinPreview_();
    } else if (name == pskl.UserSettings.MAX_FPS) {
      this.updateMaxFPS_();
    } else if (name === pskl.UserSettings.SEAMLESS_MODE) {
      this.onFrameSizeChange_();
    } else {
      this.updateZoom_();
      this.selectPreviewSizeButton_();
      this.updateContainerDimensions_();
    }
  };

  ns.PreviewController.prototype.updateOnionSkinPreview_ = function () {
    var enabledClassname = 'preview-toggle-onion-skin-enabled';
    var isEnabled = pskl.UserSettings.get(pskl.UserSettings.ONION_SKIN);

    // classList.toggle is not available on IE11.
    if (isEnabled) {
      this.toggleOnionSkinButton.classList.add(enabledClassname);
    } else {
      this.toggleOnionSkinButton.classList.remove(enabledClassname);
    }
  };

  ns.PreviewController.prototype.selectPreviewSizeButton_ = function () {
    var currentlySelected = document.querySelector('.size-button-selected');
    if (currentlySelected) {
      currentlySelected.classList.remove('size-button-selected');
    }

    var selectedSize = pskl.UserSettings.get(pskl.UserSettings.PREVIEW_SIZE);
    var previewSize = this.previewSizes[selectedSize];
    previewSize.button.classList.add('size-button-selected');
  };

  ns.PreviewController.prototype.updateMaxFPS_ = function () {
    var maxFps = pskl.UserSettings.get(pskl.UserSettings.MAX_FPS);
    this.fpsRangeInput.setAttribute('max', maxFps);
    this.piskelController.setFPS(Math.min(maxFps, this.piskelController.getFPS()));
  };

  ns.PreviewController.prototype.updateZoom_ = function () {
    var previewSize = pskl.UserSettings.get(pskl.UserSettings.PREVIEW_SIZE);

    var zoom;
    if (previewSize === 'original') {
      zoom = 1;
    } else if (previewSize === 'best') {
      zoom = Math.floor(this.calculateZoom_());
    } else if (previewSize === 'full') {
      zoom = this.calculateZoom_();
    }

    this.renderer.setZoom(zoom);
    this.setRenderFlag_(true);
  };

  ns.PreviewController.prototype.getZoom = function () {
    return this.calculateZoom_();
  };

  ns.PreviewController.prototype.getCoordinates = function(x, y) {
    var containerOffset = this.container.offset();
    x = x - containerOffset.left;
    y = y - containerOffset.top;
    var zoom = this.getZoom();
    return {
      x : Math.floor(x / zoom),
      y : Math.floor(y / zoom)
    };
  };

  /**
   * Event handler triggered on 'input' or 'change' events.
   */
  ns.PreviewController.prototype.onFpsRangeInputUpdate_ = function (evt) {
    var fps = parseInt(this.fpsRangeInput.value, 10);
    this.piskelController.setFPS(fps);
    // blur only on 'change' events, as blurring on 'input' breaks on Firefox
    if (evt.type === 'change') {
      this.fpsRangeInput.blur();
    }
  };

  ns.PreviewController.prototype.updateFPS_ = function () {
    var fps = this.piskelController.getFPS();
    if (fps !== this.fps) {
      this.fps = fps;
      // reset
      this.fpsRangeInput.value = 0;
      // set proper value
      this.fpsRangeInput.value = this.fps;
      this.fpsCounterDisplay.innerHTML = this.fps + ' FPS';
    }
  };

  ns.PreviewController.prototype.render = function (delta) {
    this.elapsedTime += delta;
    var index = this.getNextIndex_(delta);
    if (this.shouldRender_() || this.currentIndex != index) {
      this.currentIndex = index;
      var frame = pskl.utils.LayerUtils.mergeFrameAt(this.piskelController.getLayers(), index);
      this.renderer.render(frame);
      this.renderFlag = false;
      this.lastRenderTime = Date.now();

      this.popupPreviewController.render(frame);
    }
  };

  ns.PreviewController.prototype.getNextIndex_ = function (delta) {
    if (this.fps === 0) {
      return this.piskelController.getCurrentFrameIndex();
    } else {
      var index = Math.floor(this.elapsedTime / (1000 / this.fps));
      if (!this.piskelController.hasFrameAt(index)) {
        this.elapsedTime = 0;
        index = 0;
      }
      return index;
    }
  };

  /**
   * Calculate the preview zoom depending on the framesheet size
   */
  ns.PreviewController.prototype.calculateZoom_ = function () {
    var frame = this.piskelController.getCurrentFrame();
    var hZoom = PREVIEW_SIZE / frame.getHeight();
    var wZoom = PREVIEW_SIZE / frame.getWidth();

    return Math.min(hZoom, wZoom);
  };

  ns.PreviewController.prototype.onFrameSizeChange_ = function () {
    this.updateZoom_();
    this.updateContainerDimensions_();
    this.updatePreviewSizeButtons_();
  };

  ns.PreviewController.prototype.updateContainerDimensions_ = function () {
    var isSeamless = pskl.UserSettings.get(pskl.UserSettings.SEAMLESS_MODE);
    this.renderer.setRepeated(isSeamless);

    var height, width;

    if (isSeamless) {
      height = PREVIEW_SIZE;
      width = PREVIEW_SIZE;
    } else {
      var zoom = this.getZoom();
      var frame = this.piskelController.getCurrentFrame();
      height = frame.getHeight() * zoom;
      width = frame.getWidth() * zoom;
    }

    var containerEl = this.container.get(0);
    containerEl.style.height = height + 'px';
    containerEl.style.width = width + 'px';

    var horizontalMargin = (PREVIEW_SIZE - height) / 2;
    containerEl.style.marginTop = horizontalMargin + 'px';
    containerEl.style.marginBottom = horizontalMargin + 'px';

    var verticalMargin = (PREVIEW_SIZE - width) / 2;
    containerEl.style.marginLeft = verticalMargin + 'px';
    containerEl.style.marginRight = verticalMargin + 'px';
  };

  ns.PreviewController.prototype.setRenderFlag_ = function (bool) {
    this.renderFlag = bool;
  };

  ns.PreviewController.prototype.shouldRender_ = function () {
    return (this.renderFlag || this.popupPreviewController.renderFlag) &&
            (Date.now() - this.lastRenderTime > RENDER_MINIMUM_DELAY);
  };

  ns.PreviewController.prototype.toggleOnionSkin_ = function () {
    var currentValue = pskl.UserSettings.get(pskl.UserSettings.ONION_SKIN);
    pskl.UserSettings.set(pskl.UserSettings.ONION_SKIN, !currentValue);
  };
})();
<|endoftext|>


<commit_before>/**
 * Configuration file for Ranker table application
 *
 * @author  Christopher Pappas
 * @date    8.21.13
 *
 * Primary Tasks:
 *   test  :  Execute mocha tests
 *   dev    :  Development mode, file-watcher
 */

module.exports = function( grunt ) {

  grunt.initConfig({

    // + ---------------------------------------

    'simplemocha': {
      options: {
        ui: 'bdd',
        reporter: 'spec',
        compilers: 'coffee:coffee-script',
        bail: true
      },

      all: {
        src: ['test/ranker_spec.coffee']
      }
    },

    'watch': {
      src: {
        files: [ 'src/**', 'test/**' ],
        tasks: [ 'simplemocha' ]
      }
    }

  })

  // + ---------------------------------------

  grunt.registerTask( 'dev', [
    'test',
    'watch'
  ])

  grunt.registerTask( 'test', [
    'simplemocha'
  ])

  // + ---------------------------------------

  grunt.loadNpmTasks( 'grunt-contrib-watch' )
  grunt.loadNpmTasks( 'grunt-simple-mocha' )

  // + ---------------------------------------

  grunt.option( 'force', true )

}
<commit_msg>annot<commit_after>/**
 * Configuration file for Ranker table application
 *
 * Primary Tasks:
 *   test  :  Execute mocha tests
 *   dev    :  Development mode, file-watcher
 */

module.exports = function( grunt ) {

  grunt.initConfig({

    // + ---------------------------------------

    'simplemocha': {
      options: {
        ui: 'bdd',
        reporter: 'spec',
        compilers: 'coffee:coffee-script',
        bail: true
      },

      all: {
        src: ['test/ranker_spec.coffee']
      }
    },

    'watch': {
      src: {
        files: [ 'src/**', 'test/**' ],
        tasks: [ 'simplemocha' ]
      }
    }

  })

  // + ---------------------------------------

  grunt.registerTask( 'dev', [
    'test',
    'watch'
  ])

  grunt.registerTask( 'test', [
    'simplemocha'
  ])

  // + ---------------------------------------

  grunt.loadNpmTasks( 'grunt-contrib-watch' )
  grunt.loadNpmTasks( 'grunt-simple-mocha' )

  // + ---------------------------------------

  grunt.option( 'force', true )

}
<|endoftext|>


<commit_before>s.HUD = function(options) {
  this.game = options.game;

  // Add crosshairs
  this.crosshairs = new THREE.Sprite(new THREE.SpriteMaterial({
    map: s.textures.crosshairs,
    useScreenCoordinates: false,
    blending: THREE.AdditiveBlending,
    color: 0x00FF00
  }));

  this.game.camera.add(this.crosshairs);
  this.crosshairs.position.setZ(-30);

  // Create canvas
  this.canvas = document.createElement('canvas');

  this.width = this.canvas.width = window.innerWidth;
  this.height = this.canvas.height = window.innerHeight;

  this.canvas.style.position = 'absolute';
  this.canvas.style.top = 0;
  this.canvas.style.left = 0;

  this.ctx = this.canvas.getContext('2d');
  document.body.appendChild(this.canvas);

  // Update on tick
  this.update = this.update.bind(this);
  this.game.hook(this.update);

  // Set size when window size changed
  this.fitWindow = this.fitWindow.bind(this);
  $(window).on('resize', this.fitWindow);
  this.fitWindow();
};

s.HUD.baseColor = 'rgba(0, 255, 0, 0.5)';

// The height of the directional indicator (away from the circumference of the circle)
s.HUD.directionalIndicatorHeight = 20;

// The angular offset for the two sides of the directional indicator
s.HUD.directionalIndicatorAngularWidth = Math.PI/32;

s.HUD.friendlyIndicatorColor = 'rgba(0, 255, 0, 0.75)';
s.HUD.enemyIndicatorColor = 'rgba(255, 143, 0, 0.75)';
s.HUD.indicatorStroke = 'rgba(0, 0, 0, 0.5)';

s.HUD.targetColor = 'rgba(255, 0, 0, 0.5)';

// The radius of the circle around which directional circles should be drawn
s.HUD.radius = 200;

// The size of the target square relative to distance
s.HUD.squareSizeFactor = 0.02;

s.HUD.prototype.update = function() {
  // Clear canvas
  this.ctx.clearRect(0, 0, this.width, this.height);

  this.drawTarget(s.game.spaceStation.root, s.HUD.friendlyIndicatorColor, 34, 5500);

  this.drawTarget(s.game.moonBase1.root, s.HUD.enemyIndicatorColor, 34, 5500);
};

s.HUD.prototype.writeName = function(name, position, fillColor, textOffset) {
  this.ctx.fillStyle = fillColor;
  // @todo correctly center text
  this.ctx.font = 'bold 14px Andale Mono';
  var width = this.ctx.measureText(name).width;
  this.ctx.fillText(name, position.x - width / 2, position.y + textOffset);
};

s.HUD.prototype.fitWindow = function() {
  this.width = this.canvas.width = window.innerWidth;
  this.height = this.canvas.height = window.innerHeight;
  this.centerX = this.width / 2;
  this.centerY = this.height / 2;
};

s.HUD.prototype.drawTarget = function(circleTarget, fillColor, distanceFromRadius, maxBoxDistance) {
  var circleTargetInSight;
  var distanceToCircleTarget;
  var v2DcircleTarget;
  var squareSize;

  var vcircleTarget3D = circleTarget.position.clone();
  var vcircleTarget2D = s.projector.projectVector(vcircleTarget3D, s.game.camera);

  if (Math.abs(vcircleTarget2D.x) <= 0.95 && Math.abs(vcircleTarget2D.y) <= 0.95 && vcircleTarget2D.z < 1) {
    circleTargetInSight = true;
    distanceToCircleTarget = this.game.player.root.position.distanceTo(circleTarget.position);
    squareSize = Math.round((this.width - distanceToCircleTarget/100)*s.HUD.squareSizeFactor);
  }

  // circleTarget targeting reticle and targeting box
  if (!circleTargetInSight) {
    var circleTarget2D = new THREE.Vector2(vcircleTarget2D.x, vcircleTarget2D.y);
    circleTarget2D.multiplyScalar(1/circleTarget2D.length()).multiplyScalar(s.HUD.radius+distanceFromRadius);

    var directionalIndicatorCenterX = circleTarget2D.x+this.centerX
    var directionalIndicatorCenterY = -(circleTarget2D.y-this.centerY);
    // Calculate angle away from center
    var directionalIndicatorAngle = Math.atan2(directionalIndicatorCenterY-this.centerY, directionalIndicatorCenterX-this.centerX)

    if (vcircleTarget2D.z > 1) {
      var contextRotation = directionalIndicatorAngle + Math.PI/2;
      // Target is behind us
      this.ctx.beginPath();

      // Move to center
      this.ctx.translate(this.centerX, this.centerY);

      // Rotate around axis
      this.ctx.rotate(contextRotation);

      // Draw half circle
      this.ctx.arc(0, s.HUD.radius, 10, 0, Math.PI, false);
      this.ctx.fillStyle = fillColor;
      this.ctx.fill();
      this.ctx.lineWidth = 2;
      this.ctx.strokeStyle = s.HUD.indicatorStroke;

      this.ctx.stroke();

      // Move back
      this.ctx.rotate(-contextRotation);
      this.ctx.translate(-this.centerX, -this.centerY);
    }
    else {
      // Target is in front of us
      this.ctx.beginPath();

      // Calculate the tip of the triangle
      var directionalIndicatorTopX = this.centerX + (s.HUD.radius + s.HUD.directionalIndicatorHeight) * Math.cos(directionalIndicatorAngle);
      var directionalIndicatorTopY = this.centerY + (s.HUD.radius + s.HUD.directionalIndicatorHeight) * Math.sin(directionalIndicatorAngle);

      // Calculate the first vertex of the triangle, on the circumference
      var directionalIndicatorSide1X = this.centerX + s.HUD.radius * Math.cos(directionalIndicatorAngle + s.HUD.directionalIndicatorAngularWidth);
      var directionalIndicatorSide1Y = this.centerY + s.HUD.radius * Math.sin(directionalIndicatorAngle + s.HUD.directionalIndicatorAngularWidth);

      // Calculate the divot in the triangle, between the circumference and the tip
      var directionalIndicatorDivotX = this.centerX + (s.HUD.radius + s.HUD.directionalIndicatorHeight/2) * Math.cos(directionalIndicatorAngle);
      var directionalIndicatorDivotY = this.centerY + (s.HUD.radius + s.HUD.directionalIndicatorHeight/2) * Math.sin(directionalIndicatorAngle);

      // Calculate the second vertex of the triangle, on the circumference
      var directionalIndicatorSide2X = this.centerX + s.HUD.radius * Math.cos(directionalIndicatorAngle - s.HUD.directionalIndicatorAngularWidth);
      var directionalIndicatorSide2Y = this.centerY + s.HUD.radius * Math.sin(directionalIndicatorAngle - s.HUD.directionalIndicatorAngularWidth);

      // Move to tip
      this.ctx.moveTo(directionalIndicatorTopX, directionalIndicatorTopY);

      // Line to point 1
      this.ctx.lineTo(directionalIndicatorSide1X, directionalIndicatorSide1Y);

      // Line to divot
      this.ctx.lineTo(directionalIndicatorDivotX, directionalIndicatorDivotY);

      // Line to point 2
      this.ctx.lineTo(directionalIndicatorSide2X, directionalIndicatorSide2Y);

      // Line back to tip
      this.ctx.lineTo(directionalIndicatorTopX, directionalIndicatorTopY);

      this.ctx.fillStyle = fillColor;
      this.ctx.fill();
      this.ctx.lineWidth = 2;
      this.ctx.strokeStyle = s.HUD.indicatorStroke;
      this.ctx.stroke();
    }
  }

  // Draw square around object
  if (circleTargetInSight && distanceToCircleTarget > maxBoxDistance) {
    v2DcircleTarget = vcircleTarget2D.clone();
    v2DcircleTarget.x =  (this.width  + v2DcircleTarget.x*this.width )/2;
    v2DcircleTarget.y = -(-this.height + v2DcircleTarget.y*this.height)/2;

    this.ctx.strokeStyle = fillColor;
    this.ctx.lineWidth = 1;
    this.ctx.strokeRect(v2DcircleTarget.x-squareSize, v2DcircleTarget.y-squareSize, squareSize*2, squareSize*2);

    if (circleTarget.name) {
      var textOffset = squareSize * 2;
      this.writeName(circleTarget.name, v2DcircleTarget, fillColor, textOffset);
    }
    else {
      throw new Error('s.HUD: Name not defined for mesh');
    }
  }
};
<commit_msg>Make directional arrows a little larger, make directional indicators more sane when looking behind<commit_after>s.HUD = function(options) {
  this.game = options.game;

  // Add crosshairs
  this.crosshairs = new THREE.Sprite(new THREE.SpriteMaterial({
    map: s.textures.crosshairs,
    useScreenCoordinates: false,
    blending: THREE.AdditiveBlending,
    color: 0x00FF00
  }));

  this.game.camera.add(this.crosshairs);
  this.crosshairs.position.setZ(-30);

  // Create canvas
  this.canvas = document.createElement('canvas');

  this.width = this.canvas.width = window.innerWidth;
  this.height = this.canvas.height = window.innerHeight;

  this.canvas.style.position = 'absolute';
  this.canvas.style.top = 0;
  this.canvas.style.left = 0;

  this.ctx = this.canvas.getContext('2d');
  document.body.appendChild(this.canvas);

  // Update on tick
  this.update = this.update.bind(this);
  this.game.hook(this.update);

  // Set size when window size changed
  this.fitWindow = this.fitWindow.bind(this);
  $(window).on('resize', this.fitWindow);
  this.fitWindow();
};

s.HUD.baseColor = 'rgba(0, 255, 0, 0.5)';

// The height of the directional indicator (away from the circumference of the circle)
s.HUD.directionalIndicatorHeight = 23;

// The angular offset for the two sides of the directional indicator
s.HUD.directionalIndicatorAngularWidth = Math.PI/28;

s.HUD.friendlyIndicatorColor = 'rgba(0, 255, 0, 0.75)';
s.HUD.enemyIndicatorColor = 'rgba(255, 143, 0, 0.75)';
s.HUD.indicatorStroke = 'rgba(0, 0, 0, 0.5)';

s.HUD.targetColor = 'rgba(255, 0, 0, 0.5)';

// The radius of the circle around which directional circles should be drawn
s.HUD.radius = 200;

// The size of the target square relative to distance
s.HUD.squareSizeFactor = 0.02;

s.HUD.prototype.update = function() {
  // Clear canvas
  this.ctx.clearRect(0, 0, this.width, this.height);

  this.drawTarget(s.game.spaceStation.root, s.HUD.friendlyIndicatorColor, 34, 5500);

  this.drawTarget(s.game.moonBase1.root, s.HUD.enemyIndicatorColor, 34, 5500);
};

s.HUD.prototype.writeName = function(name, position, fillColor, textOffset) {
  this.ctx.fillStyle = fillColor;
  // @todo correctly center text
  this.ctx.font = 'bold 14px Andale Mono';
  var width = this.ctx.measureText(name).width;
  this.ctx.fillText(name, position.x - width / 2, position.y + textOffset);
};

s.HUD.prototype.fitWindow = function() {
  this.width = this.canvas.width = window.innerWidth;
  this.height = this.canvas.height = window.innerHeight;
  this.centerX = this.width / 2;
  this.centerY = this.height / 2;
};

s.HUD.prototype.drawTarget = function(circleTarget, fillColor, distanceFromRadius, maxBoxDistance) {
  var circleTargetInSight;
  var distanceToCircleTarget;
  var v2DcircleTarget;
  var squareSize;

  var vcircleTarget3D = circleTarget.position.clone();
  var vcircleTarget2D = s.projector.projectVector(vcircleTarget3D, s.game.camera);

  if (Math.abs(vcircleTarget2D.x) <= 0.95 && Math.abs(vcircleTarget2D.y) <= 0.95 && vcircleTarget2D.z < 1) {
    circleTargetInSight = true;
    distanceToCircleTarget = this.game.player.root.position.distanceTo(circleTarget.position);
    squareSize = Math.round((this.width - distanceToCircleTarget/100)*s.HUD.squareSizeFactor);
  }

  // circleTarget targeting reticle and targeting box
  if (!circleTargetInSight) {
    var circleTarget2D = new THREE.Vector2(vcircleTarget2D.x, vcircleTarget2D.y);
    circleTarget2D.multiplyScalar(1/circleTarget2D.length()).multiplyScalar(s.HUD.radius+distanceFromRadius);

    var directionalIndicatorCenterX = circleTarget2D.x+this.centerX
    var directionalIndicatorCenterY = -(circleTarget2D.y-this.centerY);
    // Calculate angle away from center
    var directionalIndicatorAngle = Math.atan2(directionalIndicatorCenterY-this.centerY, directionalIndicatorCenterX-this.centerX)

    var targetIsBehindUs = vcircleTarget2D.z > 1;
    if (this.game.player.viewMode === 'front') {
      targetIsBehindUs = !targetIsBehindUs;
      directionalIndicatorAngle += Math.PI;
    }

    if (targetIsBehindUs) {
      var contextRotation = directionalIndicatorAngle + Math.PI/2;
      // Target is behind us
      this.ctx.beginPath();

      // Move to center
      this.ctx.translate(this.centerX, this.centerY);

      // Rotate around axis
      this.ctx.rotate(contextRotation);

      // Draw half circle
      this.ctx.arc(0, s.HUD.radius, 10, 0, Math.PI, false);
      this.ctx.fillStyle = fillColor;
      this.ctx.fill();
      this.ctx.lineWidth = 2;
      this.ctx.strokeStyle = s.HUD.indicatorStroke;

      this.ctx.stroke();

      // Move back
      this.ctx.rotate(-contextRotation);
      this.ctx.translate(-this.centerX, -this.centerY);
    }
    else {
      // Target is in front of us
      this.ctx.beginPath();

      // Calculate the tip of the triangle
      var directionalIndicatorTopX = this.centerX + (s.HUD.radius + s.HUD.directionalIndicatorHeight) * Math.cos(directionalIndicatorAngle);
      var directionalIndicatorTopY = this.centerY + (s.HUD.radius + s.HUD.directionalIndicatorHeight) * Math.sin(directionalIndicatorAngle);

      // Calculate the first vertex of the triangle, on the circumference
      var directionalIndicatorSide1X = this.centerX + s.HUD.radius * Math.cos(directionalIndicatorAngle + s.HUD.directionalIndicatorAngularWidth);
      var directionalIndicatorSide1Y = this.centerY + s.HUD.radius * Math.sin(directionalIndicatorAngle + s.HUD.directionalIndicatorAngularWidth);

      // Calculate the divot in the triangle, between the circumference and the tip
      var directionalIndicatorDivotX = this.centerX + (s.HUD.radius + s.HUD.directionalIndicatorHeight/2) * Math.cos(directionalIndicatorAngle);
      var directionalIndicatorDivotY = this.centerY + (s.HUD.radius + s.HUD.directionalIndicatorHeight/2) * Math.sin(directionalIndicatorAngle);

      // Calculate the second vertex of the triangle, on the circumference
      var directionalIndicatorSide2X = this.centerX + s.HUD.radius * Math.cos(directionalIndicatorAngle - s.HUD.directionalIndicatorAngularWidth);
      var directionalIndicatorSide2Y = this.centerY + s.HUD.radius * Math.sin(directionalIndicatorAngle - s.HUD.directionalIndicatorAngularWidth);

      // Move to tip
      this.ctx.moveTo(directionalIndicatorTopX, directionalIndicatorTopY);

      // Line to point 1
      this.ctx.lineTo(directionalIndicatorSide1X, directionalIndicatorSide1Y);

      // Line to divot
      this.ctx.lineTo(directionalIndicatorDivotX, directionalIndicatorDivotY);

      // Line to point 2
      this.ctx.lineTo(directionalIndicatorSide2X, directionalIndicatorSide2Y);

      // Line back to tip
      this.ctx.lineTo(directionalIndicatorTopX, directionalIndicatorTopY);

      this.ctx.fillStyle = fillColor;
      this.ctx.fill();
      this.ctx.lineWidth = 2;
      this.ctx.strokeStyle = s.HUD.indicatorStroke;
      this.ctx.stroke();
    }
  }

  // Draw square around object
  if (circleTargetInSight && distanceToCircleTarget > maxBoxDistance) {
    v2DcircleTarget = vcircleTarget2D.clone();
    v2DcircleTarget.x =  (this.width  + v2DcircleTarget.x*this.width )/2;
    v2DcircleTarget.y = -(-this.height + v2DcircleTarget.y*this.height)/2;

    this.ctx.strokeStyle = fillColor;
    this.ctx.lineWidth = 1;
    this.ctx.strokeRect(v2DcircleTarget.x-squareSize, v2DcircleTarget.y-squareSize, squareSize*2, squareSize*2);

    if (circleTarget.name) {
      var textOffset = squareSize * 2;
      this.writeName(circleTarget.name, v2DcircleTarget, fillColor, textOffset);
    }
    else {
      throw new Error('s.HUD: Name not defined for mesh');
    }
  }
};
<|endoftext|>


<commit_before>// This file is part of the Shapy project.
// Licensing information can be found in the LICENSE file.
// (C) 2015 The Shapy Team. All rights reserved.
goog.provide('shapy.editor.Viewport');

goog.require('goog.math.Rect');
goog.require('goog.math.Vec2');
goog.require('goog.vec.Quaternion');

/**
 * A single viewport, our eye into the world.
 *
 * @constructor
 *
 * @param {string} name Name of the viewport.
 */
shapy.editor.Viewport = function(name) {
  /**
   * The name of the viewport.
   * @public {string}
   * @const
   */
  this.name = name;

  /**
   * The camera attached to the viewport.
   * @public {!shapy.editor.Camera}
   * @const
   */
  this.camera = new shapy.editor.Camera.Persp();

  /**
   * Control cube attached to the viewport.
   * @public {!shapy.editor.CamCube}
   * @const
   */
  this.camCube = new shapy.editor.CamCube(this, this.camera);

  /**
   * The size and position of the viewport.
   * @public {!goog.math.Size}
   * @const
   */
  this.rect = new goog.math.Rect(0, 0, 0, 0);

  /**
   * Type of the viewport.
   * @public {!shapy.editor.Viewport.Type}
   */
  this.type = shapy.editor.Viewport.Type.PERSPECTIVE;

  /**
   * Current position of the mouse.
   * @private {!goog.math.Vec2}
   */
  this.currMousePos_ = new goog.math.Vec2(0, 0);

  /**
   * Last position of the mouse.
   * @private {!goog.math.Vec2}
   */
  this.lastMousePos_ = new goog.math.Vec2(0, 0);

  /**
   * Flag indicating if the camera is rotating.
   * @private {boolean}
   */
   this.isRotating_ = false;

  /**
   * Flag indicating if the camera is panning.
   * @private {boolean}
   */
   this.isPanning_ = false;

  /**
   * Flag indicating if the viewport is active, i.e. it is highlighted.
   * @public {boolean}
   */
  this.active = false;

  /**
   * Active rig.
   * @public {shapy.editor.Rig}
   */
  this.rig = null;

  /**
   * Group select rectangle.
   * @public {goog.math.Rect}
   */
  this.group = null;

  /**
   * Last mouse click position.
   * @private {!goog.math.Vec2}
   */
  this.lastClick_ = new goog.math.Vec2(0, 0);
};

/**
 * Returns the ray that corresponds to the current mouse position.
 *
 * @private
 *
 * @param {number} x Mouse X position.
 * @param {number} y Mouse Y position.
 *
 * @return {!goog.vec.Ray.Type} Ray passing through point.
 */
shapy.editor.Viewport.prototype.raycast_ = function(x, y) {
  return this.camera.raycast(2 * x / this.rect.w - 1, 2 * y / this.rect.h - 1);
};

/**
 * Returns the frustum that corresponds to a group selection.
 *
 * @param {!goog.math.Rect} rect Selection rectangle.
 *
 * @return {!goog.vec.Ray.Type} Ray passing through point.
 */
shapy.editor.Viewport.prototype.groupcast = function(rect) {
  return this.camera.groupcast(
      2 * rect.left / this.rect.w - 1,
      2 * rect.top / this.rect.h - 1,
      2 * (rect.left + rect.width) / this.rect.w - 1,
      2 * (rect.top + rect.height) / this.rect.h - 1);
};

/**
 * Resizes the viewport, specifying a new position and size.
 *
 * @param {number} x
 * @param {number} y
 * @param {number} w
 * @param {number} h
 */
shapy.editor.Viewport.prototype.resize = function(x, y, w, h) {
  this.rect.x = x;
  this.rect.y = y;
  this.rect.w = w;
  this.rect.h = h;
  this.camCube.resize(w, h);
  this.camera.resize(w, h);
};

/**
 * Handles a mouse motion event.
 *
 * @param {number} x Mouse X coordinate.
 * @param {number} y Mouse Y coordinate.
 *
 * @return {goog.vec.Ray}
 */
shapy.editor.Viewport.prototype.mouseMove = function(x, y) {
  this.currMousePos_.x = x;
  this.currMousePos_.y = y;

  if (this.camCube.mouseMove(x, y)) {
    return null;
  }

  var ray = this.raycast_(x, y);
  if (this.isRotating_) {
    this.rotate();
    return null;
  }
  if (this.isPanning_) {
    this.pan();
    return null;
  }
  if (this.rig && this.rig.mouseMove(ray)) {
    return null;
  }
  if (this.group) {
    if (x > this.lastClick_.x) {
      this.group.width = x - this.lastClick_.x;
    } else {
      this.group.width = this.lastClick_.x - x;
      this.group.left = x;
    }

    if (y > this.lastClick_.y) {
      this.group.height = y - this.lastClick_.y;
    } else {
      this.group.height = this.lastClick_.y - y;
      this.group.top = y;
    }

    if (this.group.width > 3 && this.group.height > 3) {
      return null;
    }
  }
  return ray;
};

/**
 * Handles a mouse enter event.
 *
 * @param {number} x Mouse X coordinate.
 * @param {number} y Mouse Y coordinate.
 */
shapy.editor.Viewport.prototype.mouseEnter = function(x, y) {
  this.group = null;
  if (this.rig) {
    this.rig.mouseEnter(this.raycast_(x, y));
  }
};

/**
 * Handles a mouse leave event.
 */
shapy.editor.Viewport.prototype.mouseLeave = function() {
  this.group = null;
  if (!this.isRotating_ && !this.isPanning_ && this.rig) {
    this.rig.mouseLeave();
  }
  this.isRotating_ = false;
  this.isPanning_ = false;
};

/**
 * Handles a mouse press event.
 *
 * @param {number} x      Mouse X coordinate.
 * @param {number} y      Mouse Y coordinate.
 * @param {number} button Mouse button that was clicked.
 *
 * @return {goog.vec.Ray}
 */
shapy.editor.Viewport.prototype.mouseDown = function(x, y, button) {
  var ray = this.raycast_(x, y);
  this.lastClick_.x = x;
  this.lastClick_.y = y;
  this.currMousePos_.x = x;
  this.currMousePos_.y = y;
  this.lastMousePos_.x = x;
  this.lastMousePos_.y = y;

  if (this.camCube.mouseDown(x, y)) {
    return null;
  }

  switch (button) {
    case 1: {
      if (this.rig && this.rig.mouseDown(ray)) {
        return null;
      } else {
        this.group = new goog.math.Rect(x, y, 0, 0);
        return ray;
      }
      break;
    }
    case 2: this.isPanning_ = true; return null;
    case 3: this.isRotating_ = true; return null;
  }

  return this.raycast_(x, y);
};

/**
 * Handles a mouse release event.
 *
 * @param {number} x Mouse X coordinate.
 * @param {number} y Mouse Y coordinate.
 *
 * @return {goog.vec.Ray}
 */
shapy.editor.Viewport.prototype.mouseUp = function(x, y) {
  var ray = this.raycast_(x, y);

  this.group = null;
  this.group = null;
  if (this.camCube.mouseUp(x, y)) {
    return null;
  }

  if (!this.isRotating_ && !this.isPanning_ && this.rig) {
    if (this.rig.mouseUp(ray)) {
      return null;
    }
  }

  this.isRotating_ = false;
  this.isPanning_ = false;
  return ray;
};

/**
 * Handles a mouse wheel event.
 *
 * @param {number} delta Mouse wheel delta value.
 */
shapy.editor.Viewport.prototype.mouseWheel = function(delta) {
  var diff = goog.vec.Vec3.createFloat32();
  goog.vec.Vec3.subtract(this.camera.eye, this.camera.center, diff);
  var zoomLevel = goog.vec.Vec3.magnitude(diff);

  // Determine if zooming in or out and update accordingly.
  if (delta < 0) {
    zoomLevel /= shapy.editor.Camera.ZOOM_SPEED;
  } else {
    zoomLevel *= shapy.editor.Camera.ZOOM_SPEED;
  }

  // Clip to MIN_ZOOM.
  if (zoomLevel < shapy.editor.Camera.MIN_ZOOM) {
    zoomLevel = shapy.editor.Camera.MIN_ZOOM;
  }

  // Clip to MAX_ZOOM.
  if (zoomLevel > shapy.editor.Camera.MAX_ZOOM) {
    zoomLevel = shapy.editor.Camera.MAX_ZOOM;
  }

  // Update the position of the camera.
  goog.vec.Vec3.normalize(diff, diff);
  goog.vec.Vec3.scale(diff, zoomLevel, diff);
  goog.vec.Vec3.add(this.camera.center, diff, this.camera.eye);
};

/**
 * Recomputes the positions & sizes of all viewports.
 *
 * @param {number} kc Keycode
 */
shapy.editor.Viewport.prototype.keyDown = function(kc) {
  switch (kc) {
    case 79: // o
      this.camera = new shapy.editor.Camera.Ortho();
      this.type = shapy.editor.Viewport.Type.ORTHOGRAPHIC;
      break;

    case 80: // p
      this.camera = new shapy.editor.Camera.Persp();
      this.type = shapy.editor.Viewport.Type.PERSPECTIVE;
      break;

    default:
      break;
  }
};

/**
 * Computes a normalised vector from the center of the virtual ball to
 * the point on the virtual ball surface that is aligned with the point (x, y)
 * on the screen.
 *
 * @private
 *
 * @param {!goog.math.Vec2} pos Position of the mouse.
 *
 * @return {!goog.vec.Vec3.Type} Arcball vector.
 */
shapy.editor.Viewport.prototype.getArcballVector_ = function(pos) {
  // Convert pos to camera coordinates [-1, 1].
  var p = goog.vec.Vec3.createFloat32FromValues(
      2 * pos.x / this.rect.w - 1.0,
      2 * pos.y / this.rect.h - 1.0,
      0);

  // Compute the square of the l2 norm of p.
  var l2Squared = p[0] * p[0] + p[1] * p[1];

  // Compute the z coordinate.
  if (l2Squared < 1.0) {
    p[2] = Math.sqrt(1 - l2Squared);
  } else {
    goog.vec.Vec3.normalize(p, p);
  }

  return p;
};

/**
 * Computes the angle and axis of the camera rotation.
 */
shapy.editor.Viewport.prototype.rotate = function() {
  if (this.currMousePos_.equals(this.lastMousePos_)) {
    return;
  }

  // Compute the points at the ball surface that match the click.
  var va = this.getArcballVector_(this.lastMousePos_);
  var vb = this.getArcballVector_(this.currMousePos_);
  this.lastMousePos_.x = this.currMousePos_.x;
  this.lastMousePos_.y = this.currMousePos_.y;

  // Compute the angle.
  var angle = Math.acos(Math.min(1.0, goog.vec.Vec3.dot(va, vb)));

  // Compute the axis.
  var axis = goog.vec.Vec3.createFloat32();
  goog.vec.Vec3.cross(va, vb, axis);
  goog.vec.Mat4.multVec3NoTranslate(this.camera.invView, axis, axis);

  // Compute the rotation quaternion from the angle and the axis.
  var rotationQuater = goog.vec.Quaternion.createFloat32();
  goog.vec.Quaternion.fromAngleAxis(-angle, axis, rotationQuater);
  goog.vec.Quaternion.normalize(rotationQuater, rotationQuater);

  // Calculate the view vector.
  var viewVector = goog.vec.Vec3.createFloat32();
  goog.vec.Vec3.subtract(this.camera.eye, this.camera.center, viewVector);

  // Compute the quternion from the view vector.
  var viewQuater = goog.vec.Quaternion.createFloat32FromValues(
      viewVector[0],
      viewVector[1],
      viewVector[2],
      0);

  // Compute the new quaternion representing the rotation.
  var temp = goog.vec.Quaternion.createFloat32();
  goog.vec.Quaternion.conjugate(rotationQuater, temp);
  goog.vec.Quaternion.concat(rotationQuater, viewQuater, viewQuater);
  goog.vec.Quaternion.concat(viewQuater, temp, viewQuater);

  // Compute the updated view vector.
  goog.vec.Vec3.setFromValues(
      viewVector,
      viewQuater[0],
      viewQuater[1],
      viewQuater[2]);

  // Compute the new up vector.
  //goog.vec.Vec3.cross(viewVector, this.camera.up, this.camera.up);
  //goog.vec.Vec3.cross(this.camera.up, viewVector, this.camera.up);
  //goog.vec.Vec3.normalize(this.camera.up, this.camera.up);

  // Update the eye.
  goog.vec.Vec3.add(this.camera.center, viewVector, this.camera.eye);
};

/**
 * Performs the paning.
 */
shapy.editor.Viewport.prototype.pan = function() {
  var left = goog.vec.Vec3.createFloat32();
  var up = goog.vec.Vec3.createFloat32();
  var v = goog.vec.Vec3.createFloat32();

  // Get the normal to the plane along which to rotate.
  goog.vec.Vec3.subtract(this.camera.eye, this.camera.center, v);
  goog.vec.Vec3.cross(v, this.camera.up, left);
  goog.vec.Vec3.normalize(left, left);
  goog.vec.Vec3.cross(left, v, up);
  goog.vec.Vec3.normalize(up, up);

  // Get movement.
  var dx =
      (this.currMousePos_.x - this.lastMousePos_.x) / this.rect.w *
      shapy.editor.Camera.PAN_SPEED;
  var dy =
      (this.currMousePos_.y - this.lastMousePos_.y) / this.rect.h *
      shapy.editor.Camera.PAN_SPEED;

  // Move along x.
  goog.vec.Vec3.scale(left, dx, left);
  goog.vec.Vec3.add(this.camera.center, left, this.camera.center);
  goog.vec.Vec3.add(this.camera.eye, left, this.camera.eye);

  // Move along y.
  goog.vec.Vec3.scale(up, dy, v);
  goog.vec.Vec3.negate(v, v);
  goog.vec.Vec3.add(this.camera.center, v, this.camera.center);
  goog.vec.Vec3.add(this.camera.eye, v, this.camera.eye);

  this.lastMousePos_.x = this.currMousePos_.x;
  this.lastMousePos_.y = this.currMousePos_.y;
};

/**
 * List of viewport types.
 * @enum {number}
 */
shapy.editor.Viewport.Type = {
  LEFT: 'left',
  RIGHT: 'right',
  TOP: 'top',
  BOTTOM: 'bottom',
  FRONT: 'front',
  BACK: 'back',
  ORTHO: 'ortho',
  PERSPECTIVE: 'perp'
};
<commit_msg>Fixed camera resize.<commit_after>// This file is part of the Shapy project.
// Licensing information can be found in the LICENSE file.
// (C) 2015 The Shapy Team. All rights reserved.
goog.provide('shapy.editor.Viewport');

goog.require('goog.math.Rect');
goog.require('goog.math.Vec2');
goog.require('goog.vec.Quaternion');

/**
 * A single viewport, our eye into the world.
 *
 * @constructor
 *
 * @param {string} name Name of the viewport.
 */
shapy.editor.Viewport = function(name) {
  /**
   * The name of the viewport.
   * @public {string}
   * @const
   */
  this.name = name;

  /**
   * The camera attached to the viewport.
   * @public {!shapy.editor.Camera}
   * @const
   */
  this.camera = new shapy.editor.Camera.Persp();

  /**
   * Control cube attached to the viewport.
   * @public {!shapy.editor.CamCube}
   * @const
   */
  this.camCube = new shapy.editor.CamCube(this, this.camera);

  /**
   * The size and position of the viewport.
   * @public {!goog.math.Size}
   * @const
   */
  this.rect = new goog.math.Rect(0, 0, 0, 0);

  /**
   * Type of the viewport.
   * @public {!shapy.editor.Viewport.Type}
   */
  this.type = shapy.editor.Viewport.Type.PERSPECTIVE;

  /**
   * Current position of the mouse.
   * @private {!goog.math.Vec2}
   */
  this.currMousePos_ = new goog.math.Vec2(0, 0);

  /**
   * Last position of the mouse.
   * @private {!goog.math.Vec2}
   */
  this.lastMousePos_ = new goog.math.Vec2(0, 0);

  /**
   * Flag indicating if the camera is rotating.
   * @private {boolean}
   */
   this.isRotating_ = false;

  /**
   * Flag indicating if the camera is panning.
   * @private {boolean}
   */
   this.isPanning_ = false;

  /**
   * Flag indicating if the viewport is active, i.e. it is highlighted.
   * @public {boolean}
   */
  this.active = false;

  /**
   * Active rig.
   * @public {shapy.editor.Rig}
   */
  this.rig = null;

  /**
   * Group select rectangle.
   * @public {goog.math.Rect}
   */
  this.group = null;

  /**
   * Last mouse click position.
   * @private {!goog.math.Vec2}
   */
  this.lastClick_ = new goog.math.Vec2(0, 0);
};

/**
 * Returns the ray that corresponds to the current mouse position.
 *
 * @private
 *
 * @param {number} x Mouse X position.
 * @param {number} y Mouse Y position.
 *
 * @return {!goog.vec.Ray.Type} Ray passing through point.
 */
shapy.editor.Viewport.prototype.raycast_ = function(x, y) {
  return this.camera.raycast(2 * x / this.rect.w - 1, 2 * y / this.rect.h - 1);
};

/**
 * Returns the frustum that corresponds to a group selection.
 *
 * @param {!goog.math.Rect} rect Selection rectangle.
 *
 * @return {!goog.vec.Ray.Type} Ray passing through point.
 */
shapy.editor.Viewport.prototype.groupcast = function(rect) {
  return this.camera.groupcast(
      2 * rect.left / this.rect.w - 1,
      2 * rect.top / this.rect.h - 1,
      2 * (rect.left + rect.width) / this.rect.w - 1,
      2 * (rect.top + rect.height) / this.rect.h - 1);
};

/**
 * Resizes the viewport, specifying a new position and size.
 *
 * @param {number} x
 * @param {number} y
 * @param {number} w
 * @param {number} h
 */
shapy.editor.Viewport.prototype.resize = function(x, y, w, h) {
  this.rect.x = x;
  this.rect.y = y;
  this.rect.w = w;
  this.rect.h = h;
  this.camCube.resize(w, h);
  this.camera.resize(w, h);
};

/**
 * Handles a mouse motion event.
 *
 * @param {number} x Mouse X coordinate.
 * @param {number} y Mouse Y coordinate.
 *
 * @return {goog.vec.Ray}
 */
shapy.editor.Viewport.prototype.mouseMove = function(x, y) {
  this.currMousePos_.x = x;
  this.currMousePos_.y = y;

  if (this.camCube.mouseMove(x, y)) {
    return null;
  }

  var ray = this.raycast_(x, y);
  if (this.isRotating_) {
    this.rotate();
    return null;
  }
  if (this.isPanning_) {
    this.pan();
    return null;
  }
  if (this.rig && this.rig.mouseMove(ray)) {
    return null;
  }
  if (this.group) {
    if (x > this.lastClick_.x) {
      this.group.width = x - this.lastClick_.x;
    } else {
      this.group.width = this.lastClick_.x - x;
      this.group.left = x;
    }

    if (y > this.lastClick_.y) {
      this.group.height = y - this.lastClick_.y;
    } else {
      this.group.height = this.lastClick_.y - y;
      this.group.top = y;
    }

    if (this.group.width > 3 && this.group.height > 3) {
      return null;
    }
  }
  return ray;
};

/**
 * Handles a mouse enter event.
 *
 * @param {number} x Mouse X coordinate.
 * @param {number} y Mouse Y coordinate.
 */
shapy.editor.Viewport.prototype.mouseEnter = function(x, y) {
  this.group = null;
  if (this.rig) {
    this.rig.mouseEnter(this.raycast_(x, y));
  }
};

/**
 * Handles a mouse leave event.
 */
shapy.editor.Viewport.prototype.mouseLeave = function() {
  this.group = null;
  if (!this.isRotating_ && !this.isPanning_ && this.rig) {
    this.rig.mouseLeave();
  }
  this.isRotating_ = false;
  this.isPanning_ = false;
};

/**
 * Handles a mouse press event.
 *
 * @param {number} x      Mouse X coordinate.
 * @param {number} y      Mouse Y coordinate.
 * @param {number} button Mouse button that was clicked.
 *
 * @return {goog.vec.Ray}
 */
shapy.editor.Viewport.prototype.mouseDown = function(x, y, button) {
  var ray = this.raycast_(x, y);
  this.lastClick_.x = x;
  this.lastClick_.y = y;
  this.currMousePos_.x = x;
  this.currMousePos_.y = y;
  this.lastMousePos_.x = x;
  this.lastMousePos_.y = y;

  if (this.camCube.mouseDown(x, y)) {
    return null;
  }

  switch (button) {
    case 1: {
      if (this.rig && this.rig.mouseDown(ray)) {
        return null;
      } else {
        this.group = new goog.math.Rect(x, y, 0, 0);
        return ray;
      }
      break;
    }
    case 2: this.isPanning_ = true; return null;
    case 3: this.isRotating_ = true; return null;
  }

  return this.raycast_(x, y);
};

/**
 * Handles a mouse release event.
 *
 * @param {number} x Mouse X coordinate.
 * @param {number} y Mouse Y coordinate.
 *
 * @return {goog.vec.Ray}
 */
shapy.editor.Viewport.prototype.mouseUp = function(x, y) {
  var ray = this.raycast_(x, y);

  this.group = null;
  this.group = null;
  if (this.camCube.mouseUp(x, y)) {
    return null;
  }

  if (!this.isRotating_ && !this.isPanning_ && this.rig) {
    if (this.rig.mouseUp(ray)) {
      return null;
    }
  }

  this.isRotating_ = false;
  this.isPanning_ = false;
  return ray;
};

/**
 * Handles a mouse wheel event.
 *
 * @param {number} delta Mouse wheel delta value.
 */
shapy.editor.Viewport.prototype.mouseWheel = function(delta) {
  var diff = goog.vec.Vec3.createFloat32();
  goog.vec.Vec3.subtract(this.camera.eye, this.camera.center, diff);
  var zoomLevel = goog.vec.Vec3.magnitude(diff);

  // Determine if zooming in or out and update accordingly.
  if (delta < 0) {
    zoomLevel /= shapy.editor.Camera.ZOOM_SPEED;
  } else {
    zoomLevel *= shapy.editor.Camera.ZOOM_SPEED;
  }

  // Clip to MIN_ZOOM.
  if (zoomLevel < shapy.editor.Camera.MIN_ZOOM) {
    zoomLevel = shapy.editor.Camera.MIN_ZOOM;
  }

  // Clip to MAX_ZOOM.
  if (zoomLevel > shapy.editor.Camera.MAX_ZOOM) {
    zoomLevel = shapy.editor.Camera.MAX_ZOOM;
  }

  // Update the position of the camera.
  goog.vec.Vec3.normalize(diff, diff);
  goog.vec.Vec3.scale(diff, zoomLevel, diff);
  goog.vec.Vec3.add(this.camera.center, diff, this.camera.eye);
};

/**
 * Recomputes the positions & sizes of all viewports.
 *
 * @param {number} kc Keycode
 */
shapy.editor.Viewport.prototype.keyDown = function(kc) {
  switch (kc) {
    case 79: // o
      this.camera = new shapy.editor.Camera.Ortho();
      this.type = shapy.editor.Viewport.Type.ORTHOGRAPHIC;
      break;

    case 80: // p
      this.camera = new shapy.editor.Camera.Persp();
      this.type = shapy.editor.Viewport.Type.PERSPECTIVE;
      break;

    default:
      break;
  }

  this.camera.resize(this.rect.w, this.rect.h);
};

/**
 * Computes a normalised vector from the center of the virtual ball to
 * the point on the virtual ball surface that is aligned with the point (x, y)
 * on the screen.
 *
 * @private
 *
 * @param {!goog.math.Vec2} pos Position of the mouse.
 *
 * @return {!goog.vec.Vec3.Type} Arcball vector.
 */
shapy.editor.Viewport.prototype.getArcballVector_ = function(pos) {
  // Convert pos to camera coordinates [-1, 1].
  var p = goog.vec.Vec3.createFloat32FromValues(
      2 * pos.x / this.rect.w - 1.0,
      2 * pos.y / this.rect.h - 1.0,
      0);

  // Compute the square of the l2 norm of p.
  var l2Squared = p[0] * p[0] + p[1] * p[1];

  // Compute the z coordinate.
  if (l2Squared < 1.0) {
    p[2] = Math.sqrt(1 - l2Squared);
  } else {
    goog.vec.Vec3.normalize(p, p);
  }

  return p;
};

/**
 * Computes the angle and axis of the camera rotation.
 */
shapy.editor.Viewport.prototype.rotate = function() {
  if (this.currMousePos_.equals(this.lastMousePos_)) {
    return;
  }

  // Compute the points at the ball surface that match the click.
  var va = this.getArcballVector_(this.lastMousePos_);
  var vb = this.getArcballVector_(this.currMousePos_);
  this.lastMousePos_.x = this.currMousePos_.x;
  this.lastMousePos_.y = this.currMousePos_.y;

  // Compute the angle.
  var angle = Math.acos(Math.min(1.0, goog.vec.Vec3.dot(va, vb)));

  // Compute the axis.
  var axis = goog.vec.Vec3.createFloat32();
  goog.vec.Vec3.cross(va, vb, axis);
  goog.vec.Mat4.multVec3NoTranslate(this.camera.invView, axis, axis);

  // Compute the rotation quaternion from the angle and the axis.
  var rotationQuater = goog.vec.Quaternion.createFloat32();
  goog.vec.Quaternion.fromAngleAxis(-angle, axis, rotationQuater);
  goog.vec.Quaternion.normalize(rotationQuater, rotationQuater);

  // Calculate the view vector.
  var viewVector = goog.vec.Vec3.createFloat32();
  goog.vec.Vec3.subtract(this.camera.eye, this.camera.center, viewVector);

  // Compute the quternion from the view vector.
  var viewQuater = goog.vec.Quaternion.createFloat32FromValues(
      viewVector[0],
      viewVector[1],
      viewVector[2],
      0);

  // Compute the new quaternion representing the rotation.
  var temp = goog.vec.Quaternion.createFloat32();
  goog.vec.Quaternion.conjugate(rotationQuater, temp);
  goog.vec.Quaternion.concat(rotationQuater, viewQuater, viewQuater);
  goog.vec.Quaternion.concat(viewQuater, temp, viewQuater);

  // Compute the updated view vector.
  goog.vec.Vec3.setFromValues(
      viewVector,
      viewQuater[0],
      viewQuater[1],
      viewQuater[2]);

  // Compute the new up vector.
  //goog.vec.Vec3.cross(viewVector, this.camera.up, this.camera.up);
  //goog.vec.Vec3.cross(this.camera.up, viewVector, this.camera.up);
  //goog.vec.Vec3.normalize(this.camera.up, this.camera.up);

  // Update the eye.
  goog.vec.Vec3.add(this.camera.center, viewVector, this.camera.eye);
};

/**
 * Performs the paning.
 */
shapy.editor.Viewport.prototype.pan = function() {
  var left = goog.vec.Vec3.createFloat32();
  var up = goog.vec.Vec3.createFloat32();
  var v = goog.vec.Vec3.createFloat32();

  // Get the normal to the plane along which to rotate.
  goog.vec.Vec3.subtract(this.camera.eye, this.camera.center, v);
  goog.vec.Vec3.cross(v, this.camera.up, left);
  goog.vec.Vec3.normalize(left, left);
  goog.vec.Vec3.cross(left, v, up);
  goog.vec.Vec3.normalize(up, up);

  // Get movement.
  var dx =
      (this.currMousePos_.x - this.lastMousePos_.x) / this.rect.w *
      shapy.editor.Camera.PAN_SPEED;
  var dy =
      (this.currMousePos_.y - this.lastMousePos_.y) / this.rect.h *
      shapy.editor.Camera.PAN_SPEED;

  // Move along x.
  goog.vec.Vec3.scale(left, dx, left);
  goog.vec.Vec3.add(this.camera.center, left, this.camera.center);
  goog.vec.Vec3.add(this.camera.eye, left, this.camera.eye);

  // Move along y.
  goog.vec.Vec3.scale(up, dy, v);
  goog.vec.Vec3.negate(v, v);
  goog.vec.Vec3.add(this.camera.center, v, this.camera.center);
  goog.vec.Vec3.add(this.camera.eye, v, this.camera.eye);

  this.lastMousePos_.x = this.currMousePos_.x;
  this.lastMousePos_.y = this.currMousePos_.y;
};

/**
 * List of viewport types.
 * @enum {number}
 */
shapy.editor.Viewport.Type = {
  LEFT: 'left',
  RIGHT: 'right',
  TOP: 'top',
  BOTTOM: 'bottom',
  FRONT: 'front',
  BACK: 'back',
  ORTHO: 'ortho',
  PERSPECTIVE: 'perp'
};
<|endoftext|>


<commit_before>module.exports = function(grunt) {

    grunt.initConfig({
		pkg: grunt.file.readJSON('package.json'),

        //our uglify options
        uglify: {
			options: {
				banner: '/*! <%= pkg.name %> V<%= pkg.version %> | <%= pkg.description %> | MIT License | By: <%= pkg.author %> | <%= pkg.homepage %> | <%= grunt.template.today("yyyy-mm-dd") %> */\n'
			},
            js: {
                files: {
                    'js/feloader-<%= pkg.version %>.min.js': ['src/feloader.js'] //save over the newly created script
                }
            }
        }

    });

    //load our tasks
    //grunt.loadNpmTasks('grunt-contrib-jshint');
    //grunt.loadNpmTasks('grunt-contrib-concat');
    grunt.loadNpmTasks('grunt-contrib-uglify');

    // default tasks to run
    // grunt.registerTask('default', ['jshint', 'concat', 'uglify']);
    //grunt.registerTask('development', ['jshint']);
    //grunt.registerTask('production', ['jshint', 'concat', 'uglify']);
    grunt.registerTask('production', ['uglify']);
};<commit_msg>edit gruntfile<commit_after>module.exports = function(grunt) {

    grunt.initConfig({
		pkg: grunt.file.readJSON('package.json'),

        uglify: {
			options: {
				banner: '/*! <%= pkg.name %> V<%= pkg.version %> | <%= pkg.description %> | MIT License | By: <%= pkg.author %> | <%= pkg.homepage %> | <%= grunt.template.today("yyyy-mm-dd") %> */\n'
			},
            js: {
                files: {
                    'js/feloader-<%= pkg.version %>.min.js': ['src/feloader.js']
                }
            }
        }

    });

    //grunt.loadNpmTasks('grunt-contrib-jshint');
    //grunt.loadNpmTasks('grunt-contrib-concat');
    grunt.loadNpmTasks('grunt-contrib-uglify');

    // grunt.registerTask('default', ['jshint', 'concat', 'uglify']);
    //grunt.registerTask('development', ['jshint']);
    //grunt.registerTask('production', ['jshint', 'concat', 'uglify']);
    grunt.registerTask('production', ['uglify']);
};<|endoftext|>


<commit_before>var command = require('../command');

var DEFAULT_TIMEOUT_SECONDS = 1000;

function pathToMessageType(path) {
  return path
           .split('?')[0]
           .replace(/\/+$/, '')
           .replace(/\/+/g, '.');
}

module.exports = {
  'droonga-get': new command.HTTPRequestResponse({
    method:    'GET',
    path:      /^\/droonga\/(.+)/,
    onRequest: function(request, connection) {
      var messageType = pathToMessageType(request.params[0]);
      var body = {};
      body.timeout = body.timeout || request.query.timeout || DEFAULT_TIMEOUT_SECONDS;
      connection.emit(messageType, body);
    }
  }),
  // XXX dangerous! this must be disabled on public services.
  'droonga-post': new command.HTTPRequestResponse({
    method:    'POST',
    path:      /^\/droonga\/(.+)/,
    onRequest: function(request, connection) {
      var messageType = pathToMessageType(request.params[0]);
      var body = '';
      request.on('data', function(chunk) {
        body += chunk;
      });
      request.on('end', function() {
        body = JSON.parse(body);
        body.timeout = body.timeout || request.query.timeout || DEFAULT_TIMEOUT_SECONDS;
        body.type = body.type || 'droonga-' + messageType;
        connection.emit(messageType, body);
      });
    }
  }),
  'droonga-streaming:watch': new command.HTTPStreaming({
    dataset:            'Watch',
    path:               '/droonga-streaming/watch',
    method:             'GET',
    subscription:       'watch.subscribe',
    unsubscription:     'watch.unsubscribe',
    messageType:        'watch.publish',
    createSubscription: function(request) {
      return {
        condition: request.query.condition
      };
    },
    translate:          function(message) {
      return message;
    }
  })
};
<commit_msg>Decrease internal default timeout for droonga commands<commit_after>var command = require('../command');

var DEFAULT_TIMEOUT_SECONDS = 60;

function pathToMessageType(path) {
  return path
           .split('?')[0]
           .replace(/\/+$/, '')
           .replace(/\/+/g, '.');
}

module.exports = {
  'droonga-get': new command.HTTPRequestResponse({
    method:    'GET',
    path:      /^\/droonga\/(.+)/,
    onRequest: function(request, connection) {
      var messageType = pathToMessageType(request.params[0]);
      var body = {};
      body.timeout = body.timeout || request.query.timeout || DEFAULT_TIMEOUT_SECONDS;
      connection.emit(messageType, body);
    }
  }),
  // XXX dangerous! this must be disabled on public services.
  'droonga-post': new command.HTTPRequestResponse({
    method:    'POST',
    path:      /^\/droonga\/(.+)/,
    onRequest: function(request, connection) {
      var messageType = pathToMessageType(request.params[0]);
      var body = '';
      request.on('data', function(chunk) {
        body += chunk;
      });
      request.on('end', function() {
        body = JSON.parse(body);
        body.timeout = body.timeout || request.query.timeout || DEFAULT_TIMEOUT_SECONDS;
        body.type = body.type || 'droonga-' + messageType;
        connection.emit(messageType, body);
      });
    }
  }),
  'droonga-streaming:watch': new command.HTTPStreaming({
    dataset:            'Watch',
    path:               '/droonga-streaming/watch',
    method:             'GET',
    subscription:       'watch.subscribe',
    unsubscription:     'watch.unsubscribe',
    messageType:        'watch.publish',
    createSubscription: function(request) {
      return {
        condition: request.query.condition
      };
    },
    translate:          function(message) {
      return message;
    }
  })
};
<|endoftext|>


<commit_before>/**
 * 输出webpack配置
 * @param options
 * @returns {*}
 */
"use strict";

const fs = require('fs');
const path = require('path');
const glob = require('glob');
const webpack = require('webpack');
const chalk = require('chalk');
const merge = require('merge');

function fileExists(path) {
    return fs.existsSync(path);
}

let generateHtmlEntry = require('./config/html');

module.exports = function (options) {
    function printLog (msg) {
        options.quiet || (console.log(msg));
    }

    // 项目webpack配置
    const webpackConfigPath = path.join(options.cwd, 'webpack.config.js');
    const userWebpackConfig = fileExists(webpackConfigPath) ? require(webpackConfigPath) : {};

    // 系统默认webpack配置
    const webpackConfigDefault = require(path.join(__dirname, 'webpack.config.default.js'))(options);

    let userWebpackConfigPlugins = [];
    if (userWebpackConfig.plugins) {
        userWebpackConfigPlugins = userWebpackConfig.plugins;
        delete userWebpackConfig.plugins;
    }

    // mixin config
    let webpackConfig = merge.recursive(true, {}, webpackConfigDefault, userWebpackConfig);

    // 强制配置
    webpackConfig.output.crossOriginLoading = 'anonymous';
    webpackConfig.output.path = path.resolve(webpackConfig.context, webpackConfig.output.path);
    if (/\/$/.test(webpackConfig.output.path) === false) {
        webpackConfig.output.path += '/';
    }

    // === 插件配置
    // 插件不做merge, 否则会影响到原型链
    webpackConfig.plugins || (webpackConfig.plugins = []);
    if (Array.isArray(userWebpackConfigPlugins) && userWebpackConfigPlugins.length) {
        webpackConfig.plugins = webpackConfig.plugins.concat(userWebpackConfigPlugins);
    }

    switch (options.env) {
        case 'production':
            webpackConfig.devtool = 'cheap-module-source-map';
            webpackConfig.output.pathinfo = false;
            break;
        case 'development':
            webpackConfig.devtool = 'cheap-module-eval-source-map';
            webpackConfig.output.pathinfo = true;
            break;
    }

    if (!webpackConfig.browserslist) {
        webpackConfig.browserslist = [
            'Chrome >= 45', 'last 2 Firefox versions',
            'ie >= 8', 'Edge >= 12',
            'iOS >= 8', 'Android >= 4', 'last 2 ChromeAndroid versions'
        ];
    }

    if (!userWebpackConfig.override) { // 混合配置
        // === 配置别名
        // 在项目中可缩减引用路径
        webpackConfig.resolve || (webpackConfig.resolve = {});
        webpackConfig.resolve.alias || (webpackConfig.resolve.alias = {});

        // 用户package和webpack配置
        const pkgPath = path.join(options.cwd, 'package.json');
        const pkg = fileExists(pkgPath) ? require(pkgPath) : {};
        if (pkg.name) {
            webpackConfig.resolve.alias[pkg.name] = webpackConfig.context;
        }

        // === loader注入
        webpackConfig = require(path.join(__dirname, 'deploy/script'))(options, webpackConfig);
        webpackConfig = require(path.join(__dirname, 'deploy/style'))(options, webpackConfig);
        webpackConfig = require(path.join(__dirname, 'deploy/image'))(options, webpackConfig);
        webpackConfig = require(path.join(__dirname, 'deploy/font'))(options, webpackConfig);
        webpackConfig = require(path.join(__dirname, 'deploy/handlebars'))(options, webpackConfig);

        // 文件头banner设置
        if (pkg.name && pkg.version) {
            webpackConfig.plugins.push(new webpack.BannerPlugin({
                banner: pkg.name + ' v' + pkg.version
            }));
        }

        if (options.env === 'production') {
            webpackConfig.plugins.push(new webpack.optimize.UglifyJsPlugin({
                output: {
                    ascii_only: true
                },
                compress: {
                    warnings: false
                }
            }));
        }

        // === 生成html
        let htmlEntryArr = generateHtmlEntry(options, webpackConfig);
        if (htmlEntryArr.length) {
            Array.prototype.push.apply(webpackConfig.plugins, htmlEntryArr);
        }

        if (options.quiet !== true) {
            webpackConfig.plugins.push(new webpack.ProgressPlugin(function (percentage, msg) {
                if (process.stdout) {
                    const stream = process.stdout;
                    if (stream.isTTY) {
                        stream.cursorTo(0);
                        if (percentage === 1) {
                            stream.write('[Guido] ' + chalk.green('Bundle build is now finished.') + '\n');
                        } else {
                            stream.write('[Guido] ' + chalk.magenta(msg));
                        }
                        stream.clearLine(1);
                    }
                } else {
                    if (percentage === 1) {
                        printLog('\n[Guido] ' + chalk.green('Bundle build is now finished.'));
                    }
                }
            }));
        }
    }

    if (options.devServer) {
        webpackConfig.devServer || (webpackConfig.devServer = {});
        webpackConfig = require(path.join(__dirname, 'deploy/dev-server'))(options, webpackConfig);
        let devSeverClientUrl = 'http://' +
            webpackConfig.devServer.host + ':' +
            webpackConfig.devServer.port;

        Object.getOwnPropertyNames(webpackConfig.entry).map(function (name) {
            webpackConfig.entry[name] = []
                .concat(webpackConfig.entry[name])
                .concat('webpack-dev-server/client?' + devSeverClientUrl)
        });
    }

    delete webpackConfig.override;
    delete webpackConfig.output.templateDir;
    delete webpackConfig.output.jsDir;
    delete webpackConfig.output.cssDir;
    delete webpackConfig.output.imageDir;
    delete webpackConfig.output.fontDir;
    delete webpackConfig.browserslist;

    return webpackConfig;
};
<commit_msg>feat: no longer support ie8<commit_after>/**
 * 输出webpack配置
 * @param options
 * @returns {*}
 */
"use strict";

const fs = require('fs');
const path = require('path');
const glob = require('glob');
const webpack = require('webpack');
const chalk = require('chalk');
const merge = require('merge');

function fileExists(path) {
    return fs.existsSync(path);
}

let generateHtmlEntry = require('./config/html');

module.exports = function (options) {
    function printLog (msg) {
        options.quiet || (console.log(msg));
    }

    // 项目webpack配置
    const webpackConfigPath = path.join(options.cwd, 'webpack.config.js');
    const userWebpackConfig = fileExists(webpackConfigPath) ? require(webpackConfigPath) : {};

    // 系统默认webpack配置
    const webpackConfigDefault = require(path.join(__dirname, 'webpack.config.default.js'))(options);

    let userWebpackConfigPlugins = [];
    if (userWebpackConfig.plugins) {
        userWebpackConfigPlugins = userWebpackConfig.plugins;
        delete userWebpackConfig.plugins;
    }

    // mixin config
    let webpackConfig = merge.recursive(true, {}, webpackConfigDefault, userWebpackConfig);

    // 强制配置
    webpackConfig.output.crossOriginLoading = 'anonymous';
    webpackConfig.output.path = path.resolve(webpackConfig.context, webpackConfig.output.path);
    if (/\/$/.test(webpackConfig.output.path) === false) {
        webpackConfig.output.path += '/';
    }

    // === 插件配置
    // 插件不做merge, 否则会影响到原型链
    webpackConfig.plugins || (webpackConfig.plugins = []);
    if (Array.isArray(userWebpackConfigPlugins) && userWebpackConfigPlugins.length) {
        webpackConfig.plugins = webpackConfig.plugins.concat(userWebpackConfigPlugins);
    }

    switch (options.env) {
        case 'production':
            webpackConfig.devtool = 'cheap-module-source-map';
            webpackConfig.output.pathinfo = false;
            break;
        case 'development':
            webpackConfig.devtool = 'cheap-module-eval-source-map';
            webpackConfig.output.pathinfo = true;
            break;
    }

    if (!webpackConfig.browserslist) {
        webpackConfig.browserslist = [
            'Chrome >= 45', 'last 2 Firefox versions',
            'ie >= 9', 'Edge >= 12',
            'iOS >= 9', 'Android >= 4', 'last 2 ChromeAndroid versions'
        ];
    }

    if (!userWebpackConfig.override) { // 混合配置
        // === 配置别名
        // 在项目中可缩减引用路径
        webpackConfig.resolve || (webpackConfig.resolve = {});
        webpackConfig.resolve.alias || (webpackConfig.resolve.alias = {});

        // 用户package和webpack配置
        const pkgPath = path.join(options.cwd, 'package.json');
        const pkg = fileExists(pkgPath) ? require(pkgPath) : {};
        if (pkg.name) {
            webpackConfig.resolve.alias[pkg.name] = webpackConfig.context;
        }

        // === loader注入
        webpackConfig = require(path.join(__dirname, 'deploy/script'))(options, webpackConfig);
        webpackConfig = require(path.join(__dirname, 'deploy/style'))(options, webpackConfig);
        webpackConfig = require(path.join(__dirname, 'deploy/image'))(options, webpackConfig);
        webpackConfig = require(path.join(__dirname, 'deploy/font'))(options, webpackConfig);
        webpackConfig = require(path.join(__dirname, 'deploy/handlebars'))(options, webpackConfig);

        // 文件头banner设置
        if (pkg.name && pkg.version) {
            webpackConfig.plugins.push(new webpack.BannerPlugin({
                banner: pkg.name + ' v' + pkg.version
            }));
        }

        if (options.env === 'production') {
            webpackConfig.plugins.push(new webpack.optimize.UglifyJsPlugin({
                output: {
                    ascii_only: true
                },
                compress: {
                    warnings: false
                }
            }));
        }

        // === 生成html
        let htmlEntryArr = generateHtmlEntry(options, webpackConfig);
        if (htmlEntryArr.length) {
            Array.prototype.push.apply(webpackConfig.plugins, htmlEntryArr);
        }

        if (options.quiet !== true) {
            webpackConfig.plugins.push(new webpack.ProgressPlugin(function (percentage, msg) {
                if (process.stdout) {
                    const stream = process.stdout;
                    if (stream.isTTY) {
                        stream.cursorTo(0);
                        if (percentage === 1) {
                            stream.write('[Guido] ' + chalk.green('Bundle build is now finished.') + '\n');
                        } else {
                            stream.write('[Guido] ' + chalk.magenta(msg));
                        }
                        stream.clearLine(1);
                    }
                } else {
                    if (percentage === 1) {
                        printLog('\n[Guido] ' + chalk.green('Bundle build is now finished.'));
                    }
                }
            }));
        }
    }

    if (options.devServer) {
        webpackConfig.devServer || (webpackConfig.devServer = {});
        webpackConfig = require(path.join(__dirname, 'deploy/dev-server'))(options, webpackConfig);
        let devSeverClientUrl = 'http://' +
            webpackConfig.devServer.host + ':' +
            webpackConfig.devServer.port;

        Object.getOwnPropertyNames(webpackConfig.entry).map(function (name) {
            webpackConfig.entry[name] = []
                .concat(webpackConfig.entry[name])
                .concat('webpack-dev-server/client?' + devSeverClientUrl)
        });
    }

    delete webpackConfig.override;
    delete webpackConfig.output.templateDir;
    delete webpackConfig.output.jsDir;
    delete webpackConfig.output.cssDir;
    delete webpackConfig.output.imageDir;
    delete webpackConfig.output.fontDir;
    delete webpackConfig.browserslist;

    return webpackConfig;
};
<|endoftext|>