Raphael SketchPad不适用于触控设备

时间:2015-07-01 05:29:59

标签: javascript jquery html css

我试图将Raphael Sketch Pad整合到一个网页上,大型设备正常运行,但它不适用于触控设备。我该如何解决这个问题?我粘贴网站链接和jquery的某些部分在这里检查并给我一个反馈http://ianli.com/sketchpad/

<script type="text/javascript">
var strokes = [{
type:"path",
path:[["M",10,10],["L",390,390]],
fill:"none", "stroke":"#000000",
stroke-opacity:1,
stroke-width:5,
stroke-linecap:"round",
stroke-linejoin:"round"
}];
var sketchpad = Raphael.sketchpad("viewer", {
width: 400,
height: 400,
strokes: strokes,
editing: false
});
</script>

2 个答案:

答案 0 :(得分:1)

我遇到了同样的问题,并在其中一个草图分叉中找到了解决方案:

https://github.com/taktran/raphael-sketchpad/commit/2e91f94967159ed67e64babd42deb6cf415a5bab

这个从未发布过,但我发现当我将上面链接中突出显示的更改复制到当前版本(0.5.1)时它才有效。我希望这个fork最终会发布到新版本中。

答案 1 :(得分:0)

我能够使用this更新版本解决此问题。请在下面找到更新的 raphael.sketchpad.js 。希望这对某人有用。

&#13;
&#13;
/*
 * Raphael SketchPad
 * Version 0.5.4
 * Copyright (c) 2011 Ian Li (http://ianli.com)
 * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
 *
 * Requires:
 * jQuery	http://jquery.com
 * Raphael	http://raphaeljs.com
 * JSON		http://www.json.org/js.html
 *
 * Reference:
 * http://ianli.com/sketchpad/ for Usage
 * 
 * Versions:
 * 0.5.4: modified to allow pinch,pan and draw at the same time on mobile devices (one finger to draw, 2 fingers to pan and pinch)
 * 0.5.3  modified to solve the wrong drawing location after pinch on chrome for andoid. The solution uses setPos function to bypass the offset() method bug.
 * 0.5.2: modified to use it on IPAD and android
 * 0.5.1 - Fixed extraneous lines when first line is drawn.
 *         Thanks to http://github.com/peterkeating for the fix!
 * 0.5.0 - Added freeze_history. Fixed bug with undoing erase actions.
 * 0.4.0 - Support undo/redo of strokes, erase, and clear.
 *       - Removed input option. To make editors/viewers, set editing option to true/false, respectively.
 *         To update an input field, listen to change event and update input field with json function.
 *       - Reduce file size V1. Changed stored path info from array into a string in SVG format.
 * 0.3.0 - Added erase, supported initializing data from input field.
 * 0.2.0 - Added iPhone/iPod Touch support, onchange event, animate.
 * 0.1.0 - Started code.
 *
 * TODO:
 * - Speed up performance.
 *   - Don't store strokes in two places. _strokes and ActionHistory.current_strokes()
 *	 - Don't rebuild strokes from history with ActionHistory.current_strokes()
 * - Reduce file size.
 *   X V1. Changed stored path info from array into a string in SVG format.
 */

/**
 * We use this wrapper to control global variables.
 * The only global variable we expose is Raphael.sketchpad.
 */
(function(Raphael) {

  /**
   * Function to create SketchPad object.
   */
  var scaling = 0; //indicates if we are zooming
  var drawing = 0; //indicates if we are drawing
  Raphael.sketchpad = function(paper, options) {
    return new SketchPad(paper, options);
  }

  // Current version.
  Raphael.sketchpad.VERSION = '0.5.1';

  /**
   * The Sketchpad object.
   */
  var SketchPad = function(paper, options) {
    // Use self to reduce confusion about this.
    var self = this;

    var _options = {
      width: 100,
      height: 100,
      strokes: [],
      editing: true
    };
    jQuery.extend(_options, options);


    // The Raphael context to draw on.
    var _paper;
    if (paper.raphael && paper.raphael.constructor == Raphael.constructor) {
      _paper = paper;
    } else if (typeof paper == "string") {
      var variableName = paper;
      _paper = Raphael(paper, _options.width, _options.height);
    } else {
      throw "first argument must be a Raphael object, an element ID, an array with 3 elements";
    }

    // The Raphael SVG canvas.
    var _canvas = _paper.canvas;

    // The HTML element that contains the canvas.
    var _container = $(_canvas).parent();

    // The default pen.
    var _pen = new Pen();


    // Public Methods
    //-----------------

    self.paper = function() {
      return _paper;
    };

    self.canvas = function() {
      return _canvas;
    };

    self.container = function() {
      return _container;
    };

    self.pen = function(value) {
      if (value === undefined) {
        return _pen;
      }
      _pen = value;
      return self; // function-chaining
    };

    // Convert an SVG path into a string, so that it's smaller when JSONified.
    // This function is used by json().
    function svg_path_to_string(path) {
      var str = "";
      for (var i = 0, n = path.length; i < n; i++) {
        var point = path[i];
        str += point[0] + point[1] + "," + point[2];
      }
      return str;
    }

    // Convert a string into an SVG path. This reverses the above code.
    function string_to_svg_path(str) {
      var path = [];
      var tokens = str.split("L");

      if (tokens.length > 0) {
        var token = tokens[0].replace("M", "");
        var points = token.split(",");
        path.push(["M", parseInt(points[0]), parseInt(points[1])]);

        for (var i = 1, n = tokens.length; i < n; i++) {
          token = tokens[i];
          points = token.split(",");
          path.push(["L", parseInt(points[0]), parseInt(points[1])]);
        }
      }

      return path;
    }

    self.json = function(value) {
      if (value === undefined) {
        for (var i = 0, n = _strokes.length; i < n; i++) {
          var stroke = _strokes[i];
          if (typeof stroke.path == "object") {
            stroke.path = svg_path_to_string(stroke.path);
          }
        }
        return JSON.stringify(_strokes);
      }

      return self.strokes(JSON.parse(value));
    };

    self.strokes = function(value) {
      if (value === undefined) {
        return _strokes;
      }
      if (jQuery.isArray(value)) {
        _strokes = value;

        for (var i = 0, n = _strokes.length; i < n; i++) {
          var stroke = _strokes[i];
          if (typeof stroke.path == "string") {
            stroke.path = string_to_svg_path(stroke.path);
          }
        }

        _action_history.add({
          type: "batch",
          strokes: jQuery.merge([], _strokes) // Make a copy.
        })

        _redraw_strokes();
        _fire_change();
      }
      return self; // function-chaining
    }

    self.freeze_history = function() {
      _action_history.freeze();
    };

    self.undoable = function() {
      return _action_history.undoable();
    };

    self.undo = function() {
      if (_action_history.undoable()) {
        _action_history.undo();
        _strokes = _action_history.current_strokes();
        _redraw_strokes();
        _fire_change();
      }
      return self; // function-chaining
    };

    self.redoable = function() {
      return _action_history.redoable();
    };

    self.redo = function() {
      if (_action_history.redoable()) {
        _action_history.redo();
        _strokes = _action_history.current_strokes();
        _redraw_strokes();
        _fire_change();
      }
      return self; // function-chaining
    };

    self.clear = function() {
      _action_history.add({
        type: "clear"
      });

      _strokes = [];
      _redraw_strokes();
      _fire_change();

      return self; // function-chaining
    };

    self.animate = function(ms) {
      if (ms === undefined) {
        ms = 500;
      }

      _paper.clear();

      if (_strokes.length > 0) {
        var i = 0;

        function animate() {
          var stroke = _strokes[i];
          var type = stroke.type;
          _paper[type]()
            .attr(stroke)
            .click(_pathclick);

          i++;
          if (i < _strokes.length) {
            setTimeout(animate, ms);
          }
        };

        animate();
      }

      return self; // function-chaining
    };

    self.editing = function(mode) {
      if (mode === undefined) {
        return _options.editing;
      }

      _options.editing = mode;
      if (_options.editing) {
        if (_options.editing == "erase") {
          // Cursor is crosshair, so it looks like we can do something.
          $(_container).css("cursor", "crosshair");
          $(_container).unbind("mousedown", _mousedown);
          $(_container).unbind("mousemove", _mousemove);
          $(_container).unbind("mouseup", _mouseup);
          $(document).unbind("mouseup", _mouseup);

          // iPhone Events
          var agent = navigator.userAgent;
          if (isTouchEnabled()) {
            $(_container).unbind("touchstart", _touchstart);
            $(_container).unbind("touchmove", _touchmove);
            $(_container).unbind("touchend", _touchend);
          }
        } else {
          // Cursor is crosshair, so it looks like we can do something.
          $(_container).css("cursor", "crosshair");

          $(_container).mousedown(_mousedown);
          $(_container).mousemove(_mousemove);
          $(_container).mouseup(_mouseup);

          // Handle the case when the mouse is released outside the canvas.
          $(document).mouseup(_mouseup);

          // iPhone Events
          var agent = navigator.userAgent;
          if (isTouchEnabled()) {
            $(_container).bind("touchstart", _touchstart);
            $(_container).bind("touchmove", _touchmove);
            $(_container).bind("touchend", _touchend);
          }
        }
      } else {
        // Reverse the settings above.
        //gaucho: replaced the following line to do not erase the background.
        //$(_container).attr("style", "cursor:default"); 
        $(_container).attr("cursor", "default");
        $(_container).unbind("mousedown", _mousedown);
        $(_container).unbind("mousemove", _mousemove);
        $(_container).unbind("mouseup", _mouseup);
        $(document).unbind("mouseup", _mouseup);

        // iPhone Events
        var agent = navigator.userAgent;
        if (isTouchEnabled()) {
          $(_container).unbind("touchstart", _touchstart);
          $(_container).unbind("touchmove", _touchmove);
          $(_container).unbind("touchend", _touchend);
        }
      }

      return self; // function-chaining
    }

    // Change events
    //----------------

    var _change_fn = function() {};
    self.change = function(fn) {
      if (fn == null || fn === undefined) {
        _change_fn = function() {};
      } else if (typeof fn == "function") {
        _change_fn = fn;
      }
    };

    function _fire_change() {
      _change_fn(variableName);
    };

    // Miscellaneous methods
    //------------------

    function _redraw_strokes() {
      _paper.clear();

      for (var i = 0, n = _strokes.length; i < n; i++) {
        var stroke = _strokes[i];
        var type = stroke.type;
        _paper[type]()
          .attr(stroke)
          .click(_pathclick);
      }
    };

    function _disable_user_select() {
      $("*").css("-webkit-user-select", "none");
      $("*").css("-moz-user-select", "none");
      if (jQuery.browser.msie) {
        $("body").attr("onselectstart", "return false;");
      }
    }

    function _enable_user_select() {
      $("*").css("-webkit-user-select", "text");
      $("*").css("-moz-user-select", "text");
      if (jQuery.browser.msie) {
        $("body").removeAttr("onselectstart");
      }
    }

    // Event handlers
    //-----------------
    // We can only attach events to the container, so do it.

    function _pathclick(e) {
      if (_options.editing == "erase") {
        var stroke = this.attr();
        stroke.type = this.type;

        _action_history.add({
          type: "erase",
          stroke: stroke
        });

        for (var i = 0, n = _strokes.length; i < n; i++) {
          var s = _strokes[i];
          if (equiv(s, stroke)) {
            _strokes.splice(i, 1);
          }
        }

        _fire_change();

        this.remove();
      }
    };

    function _mousedown(e) {
      _disable_user_select();

      _pen.start(e, self);
    };

    function _mousemove(e) {
      _pen.move(e, self);
    };

    function _mouseup(e) {
      _enable_user_select();

      var path = _pen.finish(e, self);

      if (path != null) {
        // Add event when clicked.
        path.click(_pathclick);

        // Save the stroke.
        var stroke = path.attr();
        stroke.type = path.type;

        _strokes.push(stroke);

        _action_history.add({
          type: "stroke",
          stroke: stroke
        });

        _fire_change();
      }
    };

    function _touchstart(e) {
      //gaucho: the settimeout in this function inserts a delay. the delay is useful when 
      // you are placing 2 fingers on the mobile device.                        //sometimes the first finger touches the screen before the second one.
      //without a delay raphael starts to draw because it finds just one finger on the screen
      // but we were just performing a pinch on the screen!!!
      //With this delay you are able to distinguish when the user wants to draw and when he wants to pan and pinch on the screen

      var f = e.originalEvent;
      if (f.touches.length == 1) {
        setTimeout(function() {
          e = e.originalEvent;
          if (e.touches.length == 1) {
            //so, if after a timeout we still have a single finger on the screen, this means that we want for sure to start a drawing
            drawing = true;
            e.preventDefault();
            var touch = e.touches[0];
            _mousedown(touch);
          }
        }, 50, e);
      }
    }

    function _touchmove(e) {
      e = e.originalEvent;
      //if we are drawing and we have just one finger on the screen, continue to draw, otherwise no
      if ((e.touches.length == 1) && (drawing == true)) {
        e.preventDefault();
        var touch = e.touches[0];
        _mousemove(touch);
      }
    }

    function _touchend(e) {
      e = e.originalEvent;
      //if we were drawing, stop to do it, otherwise, let the pinch-pan to continue without a "preventDefault" call.			
      if (drawing) {
        drawing = false;
        e.preventDefault();
        _mouseup(e);
      }
    }

    // Setup
    //--------

    var _action_history = new ActionHistory();

    // Path data
    var _strokes = _options.strokes;
    if (jQuery.isArray(_strokes) && _strokes.length > 0) {
      _action_history.add({
        type: "init",
        strokes: jQuery.merge([], _strokes) // Make a clone.
      });
      _redraw_strokes();
    } else {
      _strokes = [];
      _redraw_strokes();
    }

    self.editing(_options.editing);
  };

  var ActionHistory = function() {
    var self = this;

    var _history = [];

    // Index of the last state.
    var _current_state = -1;

    // Index of the freeze state.
    // The freeze state is the state where actions cannot be undone.
    var _freeze_state = -1;

    // The current set of strokes if strokes were to be rebuilt from history.
    // Set to null to force refresh.
    var _current_strokes = null;

    self.add = function(action) {
      if (_current_state + 1 < _history.length) {
        _history.splice(_current_state + 1, _history.length - (_current_state + 1));
      }

      _history.push(action);
      _current_state = _history.length - 1;

      // Reset current strokes.
      _current_strokes = null;
    };

    self.freeze = function(index) {
      if (index === undefined) {
        _freeze_state = _current_state;
      } else {
        _freeze_state = index;
      }
    };

    self.undoable = function() {
      return (_current_state > -1 && _current_state > _freeze_state);
    };

    self.undo = function() {
      if (self.undoable()) {
        _current_state--;

        // Reset current strokes.
        _current_strokes = null;
      }
    };

    self.redoable = function() {
      return _current_state < _history.length - 1;
    };

    self.redo = function() {
      if (self.redoable()) {
        _current_state++;

        // Reset current strokes.
        _current_strokes = null;
      }
    };

    // Rebuild the strokes from history.
    self.current_strokes = function() {
      if (_current_strokes == null) {
        var strokes = [];
        for (var i = 0; i <= _current_state; i++) {
          var action = _history[i];
          switch (action.type) {
            case "init":
            case "json":
            case "strokes":
            case "batch":
              jQuery.merge(strokes, action.strokes);
              break;
            case "stroke":
              strokes.push(action.stroke);
              break;
            case "erase":
              for (var s = 0, n = strokes.length; s < n; s++) {
                var stroke = strokes[s];
                if (equiv(stroke, action.stroke)) {
                  strokes.splice(s, 1);
                }
              }
              break;
            case "clear":
              strokes = [];
              break;
          }
        }

        _current_strokes = strokes;
      }
      return _current_strokes;
    };
  };

  /**
   * The default Pen object.
   */
  var Pen = function() {
    var self = this;

    var _color = "#000000";
    var _opacity = 1.0;
    var _width = 5;
    var _offset = null;

    // Drawing state
    var _drawing = false;
    var _c = null;
    var _points = [];

    self.color = function(value) {
      if (value === undefined) {
        return _color;
      }

      _color = value;

      return self;
    };

    self.width = function(value) {
      if (value === undefined) {
        return _width;
      }

      if (value < Pen.MIN_WIDTH) {
        value = Pen.MIN_WIDTH;
      } else if (value > Pen.MAX_WIDTH) {
        value = Pen.MAX_WIDTH;
      }

      _width = value;

      return self;
    }

    self.opacity = function(value) {
      if (value === undefined) {
        return _opacity;
      }

      if (value < 0) {
        value = 0;
      } else if (value > 1) {
        value = 1;
      }

      _opacity = value;

      return self;
    }

    self.start = function(e, sketchpad) {
      _drawing = true;
      //this solves the bug related to offset() function returning wrong values when you pinch on chrome for android.
      var elem = $(sketchpad.container());
      _offset = findPos(elem[0]);

      var x = e.pageX - _offset[0],
        y = e.pageY - _offset[1];
      _points.push([x, y]);

      _c = sketchpad.paper().path();

      _c.attr({
        stroke: _color,
        "stroke-opacity": _opacity,
        "stroke-width": _width,
        "stroke-linecap": "round",
        "stroke-linejoin": "round"
      });
    };

    self.finish = function(e, sketchpad) {
      var path = null;

      if (_c != null) {
        if (_points.length <= 1) {
          _c.remove();
        } else {
          path = _c;
        }
      }

      _drawing = false;
      _c = null;
      _points = [];

      return path;
    };

    self.move = function(e, sketchpad) {
      if (_drawing == true) {
        var x = e.pageX - _offset[0],
          y = e.pageY - _offset[1];
        _points.push([x, y]);
        _c.attr({
          path: points_to_svg()
        });
      }
    };

    //gaucho: this function finds the position of the schetch inside the document
    //        without using the offset() function, when possibile, in order to avoid
    //        the current bug on chrome for android when you pinch on the page.
    function findPos(obj) {
      var curleft = curtop = 0;

      if (obj.offsetParent) {

        do {
          curleft += obj.offsetLeft;
          curtop += obj.offsetTop;
        } while (obj = obj.offsetParent);

        return [curleft, curtop];
      } else { //let's use the default method offset()
        var tmpOffset = obj.offset();
        return [tmpOffset.left, tmpOffset.top];
      }
    }


    function points_to_svg() {
      if (_points != null && _points.length > 1) {
        var p = _points[0];
        var path = "M" + p[0] + "," + p[1];
        for (var i = 1, n = _points.length; i < n; i++) {
          p = _points[i];
          path += "L" + p[0] + "," + p[1];
        }
        return path;
      } else {
        return "";
      }
    };
  };

  // this function identify if the device is a mobile device.
  function isTouchEnabled() {
    var agent = navigator.userAgent;
    return agent.indexOf("iPhone") > 0 ||
      agent.indexOf("iPod") > 0 ||
      agent.indexOf("iPad") > 0 ||
      agent.indexOf("Android") > 0;
  }

  Pen.MAX_WIDTH = 1000;
  Pen.MIN_WIDTH = 1;

  /**
   * Utility to generate string representation of an object.
   */
  function inspect(obj) {
    var str = "";
    for (var i in obj) {
      str += i + "=" + obj[i] + "\n";
    }
    return str;
  }

})(window.Raphael);

Raphael.fn.display = function(elements) {
  for (var i = 0, n = elements.length; i < n; i++) {
    var e = elements[i];
    var type = e.type;
    this[type]().attr(e);
  }
};


/**
 * Utility functions to compare objects by Phil Rathe.
 * http://philrathe.com/projects/equiv
 */

// Determine what is o.
function hoozit(o) {
  if (o.constructor === String) {
    return "string";

  } else if (o.constructor === Boolean) {
    return "boolean";

  } else if (o.constructor === Number) {

    if (isNaN(o)) {
      return "nan";
    } else {
      return "number";
    }

  } else if (typeof o === "undefined") {
    return "undefined";

    // consider: typeof null === object
  } else if (o === null) {
    return "null";

    // consider: typeof [] === object
  } else if (o instanceof Array) {
    return "array";

    // consider: typeof new Date() === object
  } else if (o instanceof Date) {
    return "date";

    // consider: /./ instanceof Object;
    //           /./ instanceof RegExp;
    //          typeof /./ === "function"; // => false in IE and Opera,
    //                                          true in FF and Safari
  } else if (o instanceof RegExp) {
    return "regexp";

  } else if (typeof o === "object") {
    return "object";

  } else if (o instanceof Function) {
    return "function";
  } else {
    return undefined;
  }
}

// Call the o related callback with the given arguments.
function bindCallbacks(o, callbacks, args) {
  var prop = hoozit(o);
  if (prop) {
    if (hoozit(callbacks[prop]) === "function") {
      return callbacks[prop].apply(callbacks, args);
    } else {
      return callbacks[prop]; // or undefined
    }
  }
}

// Test for equality any JavaScript type.
// Discussions and reference: http://philrathe.com/articles/equiv
// Test suites: http://philrathe.com/tests/equiv
// Author: Philippe Rathé <prathe@gmail.com>

var equiv = function() {

  var innerEquiv; // the real equiv function
  var callers = []; // stack to decide between skip/abort functions


  var callbacks = function() {

    // for string, boolean, number and null
    function useStrictEquality(b, a) {
      if (b instanceof a.constructor || a instanceof b.constructor) {
        // to catch short annotaion VS 'new' annotation of a declaration
        // e.g. var i = 1;
        //      var j = new Number(1);
        return a == b;
      } else {
        return a === b;
      }
    }

    return {
      "string": useStrictEquality,
      "boolean": useStrictEquality,
      "number": useStrictEquality,
      "null": useStrictEquality,
      "undefined": useStrictEquality,

      "nan": function(b) {
        return isNaN(b);
      },

      "date": function(b, a) {
        return hoozit(b) === "date" && a.valueOf() === b.valueOf();
      },

      "regexp": function(b, a) {
        return hoozit(b) === "regexp" &&
          a.source === b.source && // the regex itself
          a.global === b.global && // and its modifers (gmi) ...
          a.ignoreCase === b.ignoreCase &&
          a.multiline === b.multiline;
      },

      // - skip when the property is a method of an instance (OOP)
      // - abort otherwise,
      //   initial === would have catch identical references anyway
      "function": function() {
        var caller = callers[callers.length - 1];
        return caller !== Object &&
          typeof caller !== "undefined";
      },

      "array": function(b, a) {
        var i;
        var len;

        // b could be an object literal here
        if (!(hoozit(b) === "array")) {
          return false;
        }

        len = a.length;
        if (len !== b.length) { // safe and faster
          return false;
        }
        for (i = 0; i < len; i++) {
          if (!innerEquiv(a[i], b[i])) {
            return false;
          }
        }
        return true;
      },

      "object": function(b, a) {
        var i;
        var eq = true; // unless we can proove it
        var aProperties = [],
          bProperties = []; // collection of strings

        // comparing constructors is more strict than using instanceof
        if (a.constructor !== b.constructor) {
          return false;
        }

        // stack constructor before traversing properties
        callers.push(a.constructor);

        for (i in a) { // be strict: don't ensures hasOwnProperty and go deep

          aProperties.push(i); // collect a's properties

          if (!innerEquiv(a[i], b[i])) {
            eq = false;
          }
        }

        callers.pop(); // unstack, we are done

        for (i in b) {
          bProperties.push(i); // collect b's properties
        }

        // Ensures identical properties name
        return eq && innerEquiv(aProperties.sort(), bProperties.sort());
      }
    };
  }();

  innerEquiv = function() { // can take multiple arguments
    var args = Array.prototype.slice.apply(arguments);
    if (args.length < 2) {
      return true; // end transition
    }

    return (function(a, b) {
      if (a === b) {
        return true; // catch the most you can
      } else if (a === null || b === null || typeof a === "undefined" || typeof b === "undefined" || hoozit(a) !== hoozit(b)) {
        return false; // don't lose time with error prone cases
      } else {
        return bindCallbacks(a, callbacks, [b, a]);
      }

      // apply transition with (1..n) arguments
    })(args[0], args[1]) && arguments.callee.apply(this, args.splice(1, args.length - 1));
  };

  return innerEquiv;

}();
&#13;
&#13;
&#13;