/*! * jQuery sketchable | v2.3 | Luis A. Leiva | MIT license * A jQuery plugin for the jSketch drawing library. */ /** * @method $ * @description jQuery constructor. See {@link https://jquery.com} * @param {string} selector - jQuery selector. * @return {object} jQuery */ /** * @namespace $.fn * @description jQuery prototype. See {@link https://learn.jquery.com/plugins/} */ /* eslint-env browser */ /* global jQuery */ ;(function($) { // Custom namespace ID, for private data bindind. var namespace = 'sketchable'; // Begin jQuery Sketchable plugin API. var api = { /** * Initialize the selected jQuery objects. * @param {object} [opts] - Configuration (default: {@link $.fn.sketchable.defaults}). * @return {object} jQuery * @memberof $.fn.sketchable * @ignore * @protected */ init: function(opts) { var options = $.extend(true, {}, $.fn.sketchable.defaults, opts || {}); return this.each(function() { var elem = $(this), data = elem.data(namespace); // Check if element is not initialized yet. if (!data) { // Attach event listeners. elem.bind('mousedown', mousedownHandler); elem.bind('mousemove', mousemoveHandler); elem.bind('mouseup', mouseupHandler); elem.bind('touchstart', touchdownHandler); elem.bind('touchmove', touchmoveHandler); elem.bind('touchend', touchupHandler); postProcess(elem, options); } var sketch = new jSketch(this, options.graphics); // eslint-disable-line new-cap // Reconfigure element data. elem.data(namespace, { // All strokes will be stored here. strokes: [], // This will store one stroke per touching finger. coords: {}, // Date of first coord, used as time origin. // Will be initialized on drawing the first stroke. timestamp: 0, // Save a pointer to the drawing canvas (jSketch instance). sketch: sketch, // Save also a pointer to the given options. options: options, }); // Trigger init event. if (typeof options.events.init === 'function') options.events.init(elem, elem.data(namespace)); // Initialize plugins. for (var name in $.fn.sketchable.plugins) $.fn.sketchable.plugins[name](elem); }); }, /** * Get/Set user configuration of an existing jQuery Sketchable element. * @param {object} [opts] - Configuration (default: {@link $.fn.sketchable.defaults}). * @return {object} jQuery * @memberof $.fn.sketchable * @example * var $canvas = $('canvas').sketchable('config', { interactive: false }); * // Update later on: * $canvas.sketchable('config', { interactive: true }); */ config: function(opts) { if (opts) { // setter return this.each(function() { var elem = $(this), data = elem.data(namespace); data.options = $.extend(true, {}, $.fn.sketchable.defaults, data.options, opts); postProcess(elem); }); } else { // getter return $(this).data(namespace).options; } }, /** * Retrieve data associated to an existing Sketchable instance. * @param {string} [property] - Top-level data property, e.g. "instance", "sketch", "options". * @return {*} * @memberof $.fn.sketchable * @example * // Read all the data associated to this instance. * var data = $('canvas').sketchable('data'); * // Quick access to the Sketchable instance. * var inst = $('canvas').sketchable('data', 'instance'); */ data: function(property) { var data = $(this).data(namespace); if (property) { return data[property]; } else { return data; } }, /** * Get/Set drawing data strokes sequence. * @param {array} [arr] - Multidimensional array of [x,y,time,status] tuples; status = 0 (pen down) or 1 (pen up). * @return {*} Strokes object on get, jQuery instance on set (with the new data attached). * @memberof $.fn.sketchable * @example * // Getter: read associated strokes. * var strokes = $('canvas').sketchable('strokes'); * // Setter: replace associated strokes. * $('canvas').sketchable('strokes', [ [arr1], ..., [arrN] ]); */ strokes: function(arr) { if (arr) { // setter return this.each(function() { var elem = $(this), data = elem.data(namespace); data.strokes = arr; redraw(elem); }); } else { // getter var data = $(this).data(namespace); return data.strokes; } }, /** * Allow low-level manipulation of the sketchable canvas. * @param {function} callback - Callback function, invoked with 2 arguments: elem (CANVAS element) and data (private element data). * @return {object} jQuery * @memberof $.fn.sketchable * @example * $('canvas').sketchable('handler', function(elem, data) { * // do something with elem or data * }); */ handler: function(callback) { return this.each(function() { var elem = $(this), data = elem.data(namespace); callback(elem, data); }); }, /** * Clears canvas together with associated strokes data. * @return {object} jQuery * @param {boolean} [keepStrokes] - Preserve stroke data (default: false). * @memberof $.fn.sketchable * @see $.fn.sketchable.handler * @example * var $canvas = $('canvas').sketchable(); * // This will remove strokes data as well. * $canvas.clear(); * // If you only need to clear the canvas, just do: * $canvas.clear(true); * // Or, alternatively: * $canvas.sketchable('handler', function(elem, data) { * data.sketch.clear(); * }); */ clear: function(keepStrokes) { return this.each(function() { var elem = $(this), data = elem.data(namespace), options = data.options; data.sketch.clear(); if (!keepStrokes) { data.strokes = []; data.coords = {}; } if (typeof options.events.clear === 'function') options.events.clear(elem, data); }); }, /** * Reinitialize a sketchable canvas with given configuration options. * @param {object} [opts] - Configuration (default: {@link $.fn.sketchable.defaults}). * @return {object} jQuery * @memberof $.fn.sketchable * @example * var $canvas = $('canvas').sketchable(); * // Reset default state. * $canvas.sketchable('reset'); * // Reset with custom configuration. * $canvas.sketchable('reset', { interactive:false }); */ reset: function(opts) { return this.each(function() { var elem = $(this), data = elem.data(namespace), options = data.options; elem.sketchable('destroy').sketchable(opts); if (typeof options.events.reset === 'function') options.events.reset(elem, data); }); }, /** * Destroy sketchable canvas, together with strokes data and associated events. * @return {object} jQuery * @memberof $.fn.sketchable * @example * var $canvas = $('canvas').sketchable(); * // This will leave the canvas element intact. * $canvas.sketchable('destroy'); */ destroy: function() { return this.each(function() { var elem = $(this), data = elem.data(namespace), options = data.options; elem.unbind('mouseup', mouseupHandler); elem.unbind('mousemove', mousemoveHandler); elem.unbind('mousedown', mousedownHandler); elem.unbind('touchstart', touchdownHandler); elem.unbind('touchmove', touchmoveHandler); elem.unbind('touchend', touchupHandler); elem.removeData(namespace); if (options && typeof options.events.destroy === 'function') options.events.destroy(elem, data); }); }, /** * Decorate event. Will execute default event first. * @param {string} evName - Event name. * @param {function} listener - Custom event listener. * @param {string} initiator - Some identifier. * @return {object} jQuery * @memberof $.fn.sketchable * @example * // Decorate 'clear' method with `myClearFn()`, * // using 'someId' to avoid collisions with other decorators. * $('canvas').sketchable('decorate', 'clear', myClearFn, 'someId'); */ decorate: function(evName, listener, initiator) { return this.each(function() { var elem = $(this), data = elem.data(namespace), options = data.options; // Flag event override so that it doesn't get fired more than once. var overrideId = '_bound$'+ evName + '.' + initiator; if (data[overrideId]) return; data[overrideId] = true; if (options.events && typeof options.events[evName] === 'function') { // User has defined this event, so wrap it. var fn = options.events[evName]; options.events[evName] = function() { // Exec original function first, then exec our listener. fn.apply(this, arguments); listener.apply(this, arguments); }; } else { // User has not defined this event, so attach our listener. options.events[evName] = listener; } }); }, }; /** * Create a jQuery Sketchable instance. * This is a jQuery wrapper for the jSketch drawing class. * @namespace $.fn.sketchable * @param {string|object} method - Method to invoke, or a configuration object. * @return {object} jQuery * @version 2.3 * @author Luis A. Leiva * @license MIT license * @example * $('canvas').sketchable(); * $('canvas').sketchable({ interactive:false }); */ $.fn.sketchable = function(method) { var args = Array.prototype.slice.call(arguments, 1); if (typeof method === 'object' || !method) { // Constructor. return api.init.apply(this, arguments); } else if (method.indexOf('.') > -1) { // Plugin method. var actualMethod = locate(api, method); return actualMethod.apply(this, args); } else if (api[method]) { // Instance method. return api[method].apply(this, args); } else { $.error('Unknown method: ' + method); } return this; }; /** * Public API. Provides access to all methods of jQuery Sketchable instances.
* Note: This is equivalent to accessing `Sketchable.prototype` in the non-jQuery version. * @namespace $.fn.sketchable.api * @type {object} * @see Sketchable */ $.fn.sketchable.api = api; /** * Plugins store. * @namespace $.fn.sketchable.plugins * @type {object} * @example * // All plugins are created after instance initialization: * $.fn.sketchable.plugins['your-awesome-plugin'] = function($instance) { * // Do something with the jQuery Sketchable instance. * } */ $.fn.sketchable.plugins = {}; /** * Default configuration. * Note that `events.mouse*` callbacks are triggered only if interactive is set to true. * @namespace $.fn.sketchable.defaults * @type {object} * @example * // The following is the default configuration: * $('canvas').sketchable({ * interactive: true, * mouseupMovements: false, * relTimestamps: false, * multitouch: true, * cssCursors: true, * filterCoords: false, * // Event hooks. * events: { * init: function(elem, data) { * // Called when the Sketchable instance is created. * }, * destroy: function(elem, data) { * // Called when the Sketchable instance is destroyed. * }, * clear: function(elem, data) { * // Called when the canvas is cleared. * // This event includes clearing strokes data, too. * }, * mousedown: function(elem, data, evt) { * // Called when the user clicks or taps on the canvas. * }, * mousemove: function(elem, data, evt) { * // Called when the user moves the mouse or finger over the canvas. * }, * mouseup: function(elem, data, evt) { * // Called when the user lifts the mouse or finger off the canvas. * }, * }, * // Drawing options, to be used in jSketch lib. * graphics: { * firstPointSize: 3, * lineWidth: 3, * strokeStyle: '#F0F', * fillStyle: '#F0F', * lineCap: 'round', * lineJoin: 'round', * miterLimit: 10 * } * }); */ $.fn.sketchable.defaults = { // In interactive mode, it's possible to draw via mouse/pen/touch input. interactive: true, // Indicate whether non-drawing strokes should be registered as well. // Notice that the last mouseUp stroke is never recorded, as the user has already finished drawing. mouseupMovements: false, // Indicate whether timestamps should be relative (start at time 0) or absolute (start at Unix epoch). relTimestamps: false, // Enable multitouch drawing. multitouch: true, // Display CSS cursors, mainly to indicate whether the element is interactive or not. cssCursors: true, // Remove duplicated consecutive points; e.g. `(1,2)(1,2)(5,5)(1,2)` becomes `(1,2)(5,5)(1,2)`. // This is useful for touchscreens, where the same event is registered more than once. filterCoords: false, // Event hooks. events: { // init: function(elem, data) { }, // clear: function(elem, data) { }, // destroy: function(elem, data) { }, // mousedownBefore: function(elem, data, evt) { }, // mousedown: function(elem, data, evt) { }, // mousemoveBefore: function(elem, data, evt) { }, // mousemove: function(elem, data, evt) { }, // mouseupBefore: function(elem, data, evt) { }, // mouseup: function(elem, data, evt) { }, }, // Drawing options, to be used in jSketch lib. graphics: { firstPointSize: 3, lineWidth: 3, strokeStyle: '#F0F', fillStyle: '#F0F', lineCap: 'round', lineJoin: 'round', miterLimit: 10, }, }; /** * @private */ function postProcess(elem, options) { if (!options) options = elem.data(namespace).options; var domEl = elem.get(0); if (options.cssCursors) { // Visually indicate whether this element is interactive or not. domEl.style.cursor = options.interactive ? 'pointer' : 'not-allowed'; } // Fix unwanted highlight "bug". domEl.onselectstart = function() { return false; }; }; /** * @private */ function locate(obj, path) { path = path.split('.'); for (var i = 0; i < path.length; i++) { var key = path[i]; obj = obj[key]; } return obj; } /** * @private */ function getMousePos(e) { var elem = $(e.target), pos = elem.offset(); return { x: Math.round(e.pageX - pos.left), y: Math.round(e.pageY - pos.top), time: Date.now(), }; }; /** * @private */ function saveMousePos(idx, data, pt) { // Current coords are already initialized. var coords = data.coords[idx]; if (data.options.relTimestamps) { // The first timestamp is relative to initialization time; // thus fix it so that it is relative to the timestamp of the first stroke. if (data.strokes.length === 0 && coords.length === 0) data.timestamp = pt.time; pt.time -= data.timestamp; } coords.push([pt.x, pt.y, pt.time, +data.sketch.isDrawing, idx]); // Check if consecutive points should be removed. if (data.options.filterCoords && coords.length > 1) { var lastIndex = coords.length - 1; var lastCoord = coords[lastIndex]; var currCoord = coords[lastIndex - 1]; if (lastCoord[0] == currCoord[0] && lastCoord[1] == currCoord[1]) { coords.splice(lastIndex, 1); } } }; /** * @private */ function mousedownHandler(e) { if (e.originalEvent.touches) return false; downHandler(e); }; /** * @private */ function mousemoveHandler(e) { if (e.originalEvent.touches) return false; moveHandler(e); }; /** * @private */ function mouseupHandler(e) { if (e.originalEvent.touches) return false; upHandler(e); }; /** * @private */ function touchdownHandler(e) { execTouchEvent(e, downHandler); e.preventDefault(); }; /** * @private */ function touchmoveHandler(e) { execTouchEvent(e, moveHandler); e.preventDefault(); }; /** * @private */ function touchupHandler(e) { execTouchEvent(e, upHandler); e.preventDefault(); }; /** * @private */ function downHandler(e) { // Don't handle right clicks. if (e.which === 3) return false; var idx = Math.abs(e.identifier || 0), elem = $(e.target), data = elem.data(namespace), options = data.options; // Exit early if interactivity is disabled. if (!options.interactive) return; var p = getMousePos(e); if (typeof options.events.mousedownBefore === 'function') options.events.mousedownBefore(elem, data, e); // Mark visually 1st point of stroke. if (options.graphics.firstPointSize > 0) { data.sketch .beginFill(options.graphics.fillStyle) .fillCircle(p.x, p.y, options.graphics.firstPointSize) .endFill(); } data.sketch.isDrawing = true; data.sketch.beginPath(); // Ensure that coords is properly initialized. var coords = data.coords[idx]; if (!coords) coords = []; // Don't mix mouseup and mousedown in the same stroke. if (coords.length > 0) data.strokes.push(coords); // In any case, ensure that coords is properly reset/initialized. data.coords[idx] = []; saveMousePos(idx, data, p); if (typeof options.events.mousedown === 'function') options.events.mousedown(elem, data, e); }; /** * @private */ function moveHandler(e) { var idx = Math.abs(e.identifier || 0), elem = $(e.target), data = elem.data(namespace), options = data.options; // Exit early if interactivity is disabled. if (!options.interactive) return; // Grab penup strokes AFTER drawing something on the canvas for the first time. if ( (!options.mouseupMovements || data.strokes.length === 0) && !data.sketch.isDrawing ) return; var p = getMousePos(e); if (typeof options.events.mousemoveBefore === 'function') options.events.mousemoveBefore(elem, data, e); var coords = data.coords[idx]; var last = coords[coords.length - 1]; if (last) { var lineColor, lineWidth; if (data.sketch.isDrawing) { // Style for regular, pendown strokes. lineColor = options.strokeStyle; lineWidth = options.lineWidth; } else if (options.mouseupMovements) { // Style for penup strokes. lineColor = options.mouseupMovements.strokeStyle || '#DDD'; lineWidth = options.mouseupMovements.lineWidth || 1; } data.sketch.lineStyle(lineColor, lineWidth) .line(last[0], last[1], p.x, p.y) .stroke(); } saveMousePos(idx, data, p); if (typeof options.events.mousemove === 'function') options.events.mousemove(elem, data, e); }; /** * @private */ function upHandler(e) { var idx = Math.abs(e.identifier || 0), elem = $(e.target), data = elem.data(namespace), options = data.options; // Exit early if interactivity is disabled. if (!options.interactive) return; if (typeof options.events.mouseupBefore === 'function') options.events.mouseupBefore(elem, data, e); data.sketch.isDrawing = false; data.sketch.closePath(); data.strokes.push(data.coords[idx]); data.coords[idx] = []; if (typeof options.events.mouseup === 'function') options.events.mouseup(elem, data, e); }; /** * @private */ function execTouchEvent(e, callback) { var elem = $(e.target), data = elem.data(namespace), options = data.options; if (options.multitouch) { // Track all fingers. var touches = e.originalEvent.changedTouches; for (var i = 0; i < touches.length; i++) { var touch = touches[i]; callback(touch); } } else { // Track only the current finger. var touch = e.originalEvent.touches[0]; callback(touch); } }; /** * Redraw canvas according to stored strokes data. * @return {object} jQuery * @memberof $.fn.sketchable * @private */ function redraw(elem) { var data = elem.data(namespace), options = data.options, sketch = data.sketch; // Clear current canvas content, since strokes content may have changed. sketch.clear(); for (var s = 0; s < data.strokes.length; s++) { var stroke = data.strokes[s]; for (var t = 0; t < stroke.length; t++) { var currPt = stroke[t]; var nextPt = stroke[t + 1]; // By default, assume all strokes are pendown strokes. var isDrawing = true; if (currPt.length > 3) { isDrawing = currPt[3] === 1; } // Skip penup strokes, if specified. if (!isDrawing && !options.mouseupMovements) { break; } if (t === 0) { sketch.beginPath(); // Draw first point of stroke. if (options.graphics.firstPointSize > 0) { sketch.beginFill(lineColor) .fillCircle(currPt[0], currPt[1], options.graphics.firstPointSize) .endFill(); } } // Connect consecutive points with lines. if (nextPt) { var lineColor, lineWidth; if (isDrawing) { // Style for regular, pendown strokes. lineColor = options.strokeStyle; lineWidth = options.lineWidth; } else if (options.mouseupMovements) { // Style for penup strokes. lineColor = options.mouseupMovements.strokeStyle || '#DDD'; lineWidth = options.mouseupMovements.lineWidth || 1; } sketch.lineStyle(lineColor, lineWidth) .line(currPt[0], currPt[1], nextPt[0], nextPt[1]) .stroke(); } // Flag stroke change. if (t === stroke.length - 1) { sketch.closePath(); } } } } })(jQuery);