sketchable.animate.js

/*!
 * Animation plugin for Sketchable | v1.0 | Luis A. Leiva | MIT license
 */

// XXX: Requires `sketchable.utils.js` to be loaded first.

/* eslint-env browser */
/* global Event, dataBind, deepExtend */
;(function(window) {

  // Custom namespace ID, for private data bindind.
  var namespace = 'sketchable';

  /**
   * Brings animation capabilities to Sketchable elements.
   * @class
   * @version 1.0
   * @param {Sketchable} instance - Sketchable element.
   */
  function AnimateSketch(instance) {
    var self = this;
    var data = dataBind(instance.elem)[namespace];

    // Note: To animate a sketchable canvas, strokes must be set in advance.
    var sketch   = data.sketch;
    var strokes  = data.strokes;
    var events   = data.options.events;
    var graphics = data.options.graphics;

    // Reformat strokes to handle multitouch.
    var fmtStrokes = [];
    for (var s = 0; s < strokes.length; s++) {
      var coords = strokes[s];
      for (var c = 0; c < coords.length; c++) {
        var pt = toPoint(coords[c]);
        // The strokeId is not available in jsketchable < 2.2, so add it.
        if (!pt.strokeId) pt.strokeId = s;
        fmtStrokes.push(pt);
      }
    }

    if (typeof events.animationstart === 'function')
      events.animationstart(instance, data);

    var raf;
    var frame = 0;

    sketch.lineStyle(graphics.strokeStyle, graphics.lineWidth);

    (function loop() {
      raf = requestAnimationFrame(loop);
      // Here be dragons, thus surround by try/catch.
      try {
        drawLine(sketch, fmtStrokes, frame, graphics);
      } catch (err) {
        console.error(err);
        cancelAnimationFrame(raf);
      }
      // Advance local count and check if current animation should end.
      if (++frame === fmtStrokes.length - 1) {
        cancelAnimationFrame(raf);
        if (typeof events.animationend === 'function')
          events.animationend(instance, data);
      }
    })();

    /**
     * Cancel current animation.
     * @return {AnimateSketch}.
     */
    this.cancel = function() {
      cancelAnimationFrame(raf);
      return this;
    };

    /**
     * Draw line on jSketch canvas at time t.
     * @private
     * @param {object} sketch - jSketch canvas.
     * @param {array} coords - Stroke coordinates.
     * @param {number} t - Time iterator.
     */
    function drawLine(sketch, coords, t) {
      var currPt = coords[t];
      var nextPt = coords[t + 1];

      if (t === 0 || currPt.strokeId !== nextPt.strokeId) {
        // Draw first point.
        if (sketch.data.firstPointSize) {
          var pt = t > 0 ? nextPt : currPt;
          sketch.beginFill(sketch.data.strokeStyle)
            .fillCircle(pt.x, pt.y, sketch.data.firstPointSize)
            .endFill();
        }
        // Trigger step event for subsequent strokes.
        if (t > 0 && typeof events.animationstep === 'function')
          events.animationstep(instance, data);
        // Flag stroke change.
        sketch.closePath().beginPath();
      }

      if (currPt.strokeId === nextPt.strokeId)
        sketch.line(currPt.x, currPt.y, nextPt.x, nextPt.y).stroke();
    }

    /**
     * Convert point array to object.
     * @private
     * @param {array} p - Point, having at least [x,y,t] items.
     * @return {object}
     */
    function toPoint(p) {
      if (!(p instanceof Array)) return p;
      // Point coords is an array with 4 items: [x, y, time, is_drawing, strokeId].
      return { x: p[0], y: p[1], time: p[2], strokeId: p[4] };
    }
  }

  /**
   * Animate plugin constructor for jQuery Sketchable instances.
   * @param {Sketchable} instance - Sketchable element.
   * @namespace Sketchable.plugins.animate
   */
  Sketchable.prototype.plugins.animate = function(instance) {
    var callbacks = {
      clear: function(elem, data) {
        data.animate && data.animate.cancel();
      },
      destroy: function(elem, data) {
        data.animate && data.animate.cancel();
      },
    };

    // Note: the init event is used to create sketchable instances,
    // therefore it should NOT be overriden.
    var events = 'clear destroy'.split(' ');
    for (var i = 0; i < events.length; i++) {
      var evName = events[i];
      instance.decorate(evName, callbacks[evName], 'animate');
    }

    // Expose public API: all Sketchable instances will have these methods.
    deepExtend(instance, {
      // Namespace methods to avoid collisions with other plugins.
      animate: {
        /**
         * Animate canvas strokes.
         * @return {Sketchable} Sketchable element.
         * @memberof Sketchable.plugins.animate
         * @example sketchableInstance.strokes(strokeArray).animate.strokes();
         * @example
         * // Accessing event hooks:
         * sketchableInstance.config({
         *   events: {
         *     animationstart: function(elem, data) {
         *       // Animation started: the first stroke is being drawn.
         *     },
         *     animationstep: function(elem, data) {
         *       // Animation steped: a new stroke is being drawn.
         *     },
         *     animationend: function(elem, data) {
         *       // Animation ended: the last stroke has been drawn.
         *     },
         *   }
         * })
         * .strokes(strokeArray)
         * .clear(true)
         * .animate.strokes();
         */
        strokes: function() {
          var data = dataBind(instance.elem)[namespace];
          data.animate = new AnimateSketch(instance);
          return instance;
        },
      },
    });

  };

})(this);