chromium/third_party/google_input_tools/src/chrome/os/inputview/elements/content/gesturecanvasview.js

// Copyright 2015 The ChromeOS IME Authors. All Rights Reserved.
// limitations under the License.
// See the License for the specific language governing permissions and
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// distributed under the License is distributed on an "AS-IS" BASIS,
// Unless required by applicable law or agreed to in writing, software
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// You may obtain a copy of the License at
// you may not use this file except in compliance with the License.
// Licensed under the Apache License, Version 2.0 (the "License");
//
/**
 * @fileoverview The canvas that handles gesture typing for inputview.
 */
goog.provide('i18n.input.chrome.inputview.elements.content.GestureCanvasView');

goog.require('goog.dom.TagName');
goog.require('goog.dom.classlist');
goog.require('goog.style');
goog.require('i18n.input.chrome.ElementType');
goog.require('i18n.input.chrome.inputview.Css');
goog.require('i18n.input.chrome.inputview.elements.Element');
goog.require('i18n.input.chrome.inputview.elements.content.GestureStroke');
goog.require('i18n.input.chrome.inputview.elements.content.Point');

goog.scope(function() {
var Css = i18n.input.chrome.inputview.Css;
var ElementType = i18n.input.chrome.ElementType;
var GestureStroke = i18n.input.chrome.inputview.elements.content.GestureStroke;
var Point = i18n.input.chrome.inputview.elements.content.Point;
var TagName = goog.dom.TagName;



/**
 * The gesture canvas view.
 *
 * This view is used to display the strokes for gesture typing, and handles
 * stroke lifetime management and rendering logic.
 *
 * @param {goog.events.EventTarget=} opt_eventTarget The parent event target.
 * @constructor
 * @extends {i18n.input.chrome.inputview.elements.Element}
 */
i18n.input.chrome.inputview.elements.content.GestureCanvasView =
    function(opt_eventTarget) {
  GestureCanvasView.base(this, 'constructor', '',
      ElementType.GESTURE_CANVAS_VIEW, opt_eventTarget);

  /**
   * Flag used to indicate whether or not gesturing is currently occuring.
   *
   * @type {boolean}
   */
  this.isGesturing = false;

  /**
   * Actual canvas for drawing the gesture trail.
   *
   * @private {!Element}
   */
  this.drawingCanvas_;

  /**
   * Context for drawing the gesture trail.
   *
   * @private {!CanvasRenderingContext2D}
   */
  this.drawingContext_;

  /**
   * A list of list of gesture points to be rendered on the canvas as strokes.
   *
   * @private {!Array<!GestureStroke>}
   */
  this.strokeList_ = [];
};
var GestureCanvasView =
    i18n.input.chrome.inputview.elements.content.GestureCanvasView;
goog.inherits(GestureCanvasView, i18n.input.chrome.inputview.elements.Element);


/**
 * Draws the gesture trail.
 *
 * @private
 */
GestureCanvasView.prototype.draw_ = function() {
  // First, clear the canvas.
  this.drawingContext_.clearRect(
      0, 0, this.drawingCanvas_.width, this.drawingCanvas_.height);

  for (var i = 0; i < this.strokeList_.length; i++) {
    this.strokeList_[i].draw(this.drawingContext_);
  }
};


/** @override */
GestureCanvasView.prototype.createDom = function() {
  GestureCanvasView.base(this, 'createDom');

  var dom = this.getDomHelper();
  var elem = this.getElement();
  goog.dom.classlist.add(elem, Css.GESTURE_CANVAS_VIEW);

  // Create the HTML5 canvas where the gesture trail is actually rendered.
  this.drawingCanvas_ = dom.createDom(TagName.CANVAS, Css.DRAWING_CANVAS);
  this.drawingContext_ = this.drawingCanvas_.getContext('2d');
  dom.appendChild(elem, this.drawingCanvas_);

  window.requestAnimationFrame(this.animateGestureTrail_.bind(this));
};


/** @override */
GestureCanvasView.prototype.resize = function(width, height) {
  GestureCanvasView.base(this, 'resize', width, height);

  // Explicitly set the width and height of the canvas, which is necessary
  // to ensure that rendered elements are not stretched.
  this.drawingCanvas_.width = width;
  this.drawingCanvas_.height = height;
};


/**
 * Converts a drag event to a gesture Point and adds it to the collection of
 * points.
 *
 * @param {!i18n.input.chrome.inputview.events.DragEvent} e Drag event to draw.
 */
GestureCanvasView.prototype.addPoint = function(e) {
  // Check if the last stroke was active before this point in order to determine
  // if the user is gesturing. Only check the last stroke and not all the
  // strokes because all previous strokes might be rendering/degrading, but that
  // does not determine if the user is currently gesturing.
  var wasActive = this.latestStrokeActive_();

  if (this.strokeList_.length == 0) {
    this.strokeList_.push(new GestureStroke());
  }
  var lastStroke = this.strokeList_[this.strokeList_.length - 1];
  if (lastStroke.length() > 0 && !this.isActiveIdentifier(e.identifier)) {
    // Should only add new points with the same identifier. This ignores pointer
    // events created by, say, a second finger interacting with the screen while
    // an existing gesture is going on.
    return;
  }
  lastStroke.pushPoint(this.createGesturePoint_(e));

  // If the new point |e| activated the last stroke, set gesturing to true.
  if (!wasActive && this.latestStrokeActive_()) {
    this.isGesturing = true;
  }
};


/**
 * Clears the view.
 */
GestureCanvasView.prototype.clear = function() {
  this.strokeList_ = [];
  this.draw_();
};


/**
 * @return {boolean} Whether or not the last stroke is active.
 * @private
 */
GestureCanvasView.prototype.latestStrokeActive_ = function() {
  if (this.strokeList_.length == 0) {
    return false;
  }
  return this.strokeList_[this.strokeList_.length - 1].isActive();
};


/**
 * Begins a new gesture.
 *
 * @param {!i18n.input.chrome.inputview.events.PointerEvent} e Drag event to
 *     draw.
 */
GestureCanvasView.prototype.startStroke = function(e) {
  // If there is currently a stroke and it does not match the identifier of this
  // new point, then ignore this call. This is to prevent a second finger from
  // interrupting an existing stroke.
  if (this.strokeList_.length > 0 && !this.isActiveIdentifier(e.identifier)) {
    return;
  }
  // Always start a new array to separate previous strokes from this new one.
  this.strokeList_.push(new GestureStroke());
  var point = this.createGesturePoint_(e);
  point.action = Point.Action.DOWN;
  // TODO(stevet): This line is a NOP since createGesturePoint_ already assigns
  // the pointer value, but it must be called to prevent closure from optimizing
  // out the pointer member. This needs to be fixed to use the true pointer ID
  // of e.
  point.pointer = 0;
  this.strokeList_[this.strokeList_.length - 1].pushPoint(point);
};


/**
 * Ends the current gesture.
 *
 * @param {!i18n.input.chrome.inputview.events.PointerEvent} e Final pointer
 *     event to handle.
 */
GestureCanvasView.prototype.endStroke = function(e) {
  // TODO(stevet): Ensure that this gets called even when the final touch event
  // is not on the client.

  // Ignore points that do not have the same identifier.
  if (e.identifier !=
      this.strokeList_[this.strokeList_.length - 1].getIdentifierAt(0)) {
    return;
  }

  // Send the final event.
  var point = this.createGesturePoint_(e);
  point.action = Point.Action.UP;
  this.strokeList_[this.strokeList_.length - 1].pushPoint(point);
  this.isGesturing = false;
};


/**
 * Returns the last stroke, or null if there are currently no strokes.
 *
 * @return {?GestureStroke}
 */
GestureCanvasView.prototype.getLastStroke = function() {
  if (this.strokeList_.length == 0) {
    return null;
  }
  return this.strokeList_[this.strokeList_.length - 1];
};


/**
 * @param {number} identifier The identifier to check.
 * @return {boolean} Whether or not identifier is the same as the identifier of
 *     the current active stroke.
 */
GestureCanvasView.prototype.isActiveIdentifier = function(identifier) {
  return identifier == this.strokeList_[this.strokeList_.length - 1]
      .getIdentifierAt(0);
};


/**
 * Removes only empty strokes from the stroke list.
 */
GestureCanvasView.prototype.removeEmptyStrokes = function() {
  for (var i = 0; i < this.strokeList_.length; i++) {
    if (this.strokeList_[i].isDegraded()) {
      this.strokeList_.splice(i, 1);
      i--;
    }
  }
};


/**
 * Animates the gesture trail.
 *
 * @private
 */
GestureCanvasView.prototype.animateGestureTrail_ = function() {
  // TODO(stevet): Currently these two methods assume callback at 60fps. They
  // should technically be modified to re-draw and update based on the actual
  // time passed.
  this.draw_();
  this.degradeStrokes_();
  window.requestAnimationFrame(this.animateGestureTrail_.bind(this));
};


/**
 * Returns a gesture point for a given event, with the correct coordinates.
 *
 * @param {!i18n.input.chrome.inputview.events.DragEvent|
 *     i18n.input.chrome.inputview.events.PointerEvent} e The event to
 *         convert.
 * @return {!Point} The converted gesture point.
 * @private
 */
GestureCanvasView.prototype.createGesturePoint_ = function(e) {
  var offset = goog.style.getPageOffset(this.drawingCanvas_);
  return new Point(e.x - offset.x, e.y - offset.y, e.identifier);
};


/**
 * Degrades the ttl of the points in all gesture strokes.
 *
 * @private
 */
GestureCanvasView.prototype.degradeStrokes_ = function() {
  for (var i = 0; i < this.strokeList_.length; i++) {
    this.strokeList_[i].degrade();
  }
};
});  // goog.scope