/* * jQuery.eraser v0.5.2 * makes any image or canvas erasable by the user, using touch or mouse input * https://github.com/boblemarin/jQuery.eraser * * Usage: * * $('#myImage').eraser(); // simple way * * $('#canvas').eraser( { * size: 20, // define brush size (default value is 40) * completeRatio: .65, // allows to call function when a erased ratio is reached (between 0 and 1, default is .7 ) * completeFunction: myFunction // callback function when complete ratio is reached * } ); * * $('#image').eraser( 'clear' ); // erases all canvas content * * $('#image').eraser( 'reset' ); // revert back to original content * * $('#image').eraser( 'size', 80 ); // change the eraser size * * $('#image').eraser( 'enable/disable' ); // enable or disable erasing * * $('#image').eraser( 'enabled' ); // returns whether the eraser is enabled * * * https://github.com/boblemarin/jQuery.eraser * http://minimal.be/lab/jQuery.eraser/ * * Copyright (c) 2010 boblemarin emeric@minimal.be http://www.minimal.be * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ (function($){ var methods = { init: function(options) { return this.each(function(){ var $this = $(this), data = $this.data('eraser'); if (!data) { var handleImage = function() { var $canvas = $(''), canvas = $canvas.get(0), ctx = canvas.getContext('2d'), // calculate scale ratio for high DPI devices // http://www.html5rocks.com/en/tutorials/canvas/hidpi/ devicePixelRatio = window.devicePixelRatio || 1, backingStoreRatio = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1, scaleRatio = devicePixelRatio / backingStoreRatio, realWidth = $this.width(), realHeight = $this.height(), width = realWidth * scaleRatio, height = realHeight * scaleRatio, pos = $this.offset(), enabled = (options && options.enabled === false) ? false : true, size = ((options && options.size) ? options.size : 40) * scaleRatio, completeRatio = (options && options.completeRatio) ? options.completeRatio : .7, completeFunction = (options && options.completeFunction) ? options.completeFunction : null, progressFunction = (options && options.progressFunction) ? options.progressFunction : null, zIndex = $this.css('z-index') == "auto"?1:$this.css('z-index'), parts = [], colParts = Math.floor(width / size), numParts = colParts * Math.floor(height / size), n = numParts, that = $this[0]; // replace target with canvas $this.after($canvas); canvas.id = that.id; canvas.className = that.className; canvas.width = width; canvas.height = height; canvas.style.width = realWidth.toString() + "px"; canvas.style.height = realHeight.toString() + "px"; ctx.drawImage(that, 0, 0, width, height); $this.remove(); // prepare context for drawing operations ctx.globalCompositeOperation = 'destination-out'; ctx.strokeStyle = 'rgba(255,0,0,255)'; ctx.lineWidth = size; ctx.lineCap = 'round'; // bind events $canvas.bind('mousedown.eraser', methods.mouseDown); $canvas.bind('touchstart.eraser', methods.touchStart); $canvas.bind('touchmove.eraser', methods.touchMove); $canvas.bind('touchend.eraser', methods.touchEnd); // reset parts while(n--) parts.push(1); // store values data = { posX: pos.left, posY: pos.top, touchDown: false, touchID: -999, touchX: 0, touchY: 0, ptouchX: 0, ptouchY: 0, canvas: $canvas, ctx: ctx, w: width, h: height, scaleRatio: scaleRatio, source: that, size: size, parts: parts, colParts: colParts, numParts: numParts, ratio: 0, enabled: enabled, complete: false, completeRatio: completeRatio, completeFunction: completeFunction, progressFunction: progressFunction, zIndex: zIndex }; $canvas.data('eraser', data); // listen for resize event to update offset values $(window).resize(function() { var pos = $canvas.offset(); data.posX = pos.left; data.posY = pos.top; }); } if (this.complete && this.naturalWidth > 0) { handleImage(); } else { //this.onload = handleImage; $this.on('load', handleImage); } } }); }, touchStart: function(event) { var $this = $(this), data = $this.data('eraser'); if (!data.touchDown) { var t = event.originalEvent.changedTouches[0], tx = t.pageX - data.posX, ty = t.pageY - data.posY; tx *= data.scaleRatio; ty *= data.scaleRatio; if (data.enabled) { methods.evaluatePoint(data, tx, ty); } data.touchDown = true; data.touchID = t.identifier; data.touchX = tx; data.touchY = ty; event.preventDefault(); } }, touchMove: function(event) { var $this = $(this), data = $this.data('eraser'); if (data.touchDown) { var ta = event.originalEvent.changedTouches, n = ta.length; while (n--) { if (ta[n].identifier == data.touchID) { var tx = ta[n].pageX - data.posX, ty = ta[n].pageY - data.posY; tx *= data.scaleRatio; ty *= data.scaleRatio; if (data.enabled) { methods.evaluatePoint(data, tx, ty); data.ctx.beginPath(); data.ctx.moveTo(data.touchX, data.touchY); data.ctx.lineTo(tx, ty); data.ctx.stroke(); $this.css({"z-index":$this.css('z-index')==data.zIndex?parseInt(data.zIndex)+1:data.zIndex}); } data.touchX = tx; data.touchY = ty; event.preventDefault(); break; } } } }, touchEnd: function(event) { var $this = $(this), data = $this.data('eraser'); if ( data.touchDown ) { var ta = event.originalEvent.changedTouches, n = ta.length; while( n-- ) { if ( ta[n].identifier == data.touchID ) { data.touchDown = false; event.preventDefault(); break; } } } }, evaluatePoint: function(data, tx, ty) { if (!data.enabled) return; var p = Math.floor(tx/data.size) + Math.floor( ty / data.size ) * data.colParts; if ( p >= 0 && p < data.numParts ) { data.ratio += data.parts[p]; data.parts[p] = 0; if (!data.complete) { p = data.ratio/data.numParts; if ( p >= data.completeRatio ) { data.complete = true; if ( data.completeFunction != null ) data.completeFunction(); } else { if ( data.progressFunction != null ) data.progressFunction(p); } } } }, mouseDown: function(event) { var $this = $(this), data = $this.data('eraser'), tx = event.pageX - data.posX, ty = event.pageY - data.posY; tx *= data.scaleRatio; ty *= data.scaleRatio; data.touchDown = true; data.touchX = tx; data.touchY = ty; if (data.enabled) { methods.evaluatePoint( data, tx, ty ); data.ctx.beginPath(); data.ctx.moveTo(data.touchX-1, data.touchY); data.ctx.lineTo(data.touchX, data.touchY); data.ctx.stroke(); } $this.bind('mousemove.eraser', methods.mouseMove); $(document).bind('mouseup.eraser', data, methods.mouseUp); event.preventDefault(); }, mouseMove: function(event) { var $this = $(this), data = $this.data('eraser'), tx = event.pageX - data.posX, ty = event.pageY - data.posY; tx *= data.scaleRatio; ty *= data.scaleRatio; if (data.enabled) { methods.evaluatePoint( data, tx, ty ); data.ctx.beginPath(); data.ctx.moveTo( data.touchX, data.touchY ); data.ctx.lineTo( tx, ty ); data.ctx.stroke(); $this.css({"z-index":$this.css('z-index')==data.zIndex?parseInt(data.zIndex)+1:data.zIndex}); } data.touchX = tx; data.touchY = ty; event.preventDefault(); }, mouseUp: function(event) { var data = event.data, $this = data.canvas; data.touchDown = false; $this.unbind('mousemove.eraser'); $(document).unbind('mouseup.eraser'); event.preventDefault(); }, clear: function() { var $this = $(this), data = $this.data('eraser'); if (data) { data.ctx.clearRect(0, 0, data.w, data.h); var n = data.numParts; while(n--) data.parts[n] = 0; data.ratio = data.numParts; data.complete = true; if (data.completeFunction != null) data.completeFunction(); } }, enabled: function() { var $this = $(this), data = $this.data('eraser'); if (data && data.enabled) { return true; } return false; }, enable: function() { var $this = $(this), data = $this.data('eraser'); if (data) { data.enabled = true; } }, disable: function() { var $this = $(this), data = $this.data('eraser'); if (data) { data.enabled = false; } }, size: function(value) { var $this = $(this), data = $this.data('eraser'); if (data && value) { data.size = value; data.ctx.lineWidth = value; } }, reset: function() { var $this = $(this), data = $this.data('eraser'); if (data) { data.ctx.globalCompositeOperation = 'source-over'; data.ctx.drawImage( data.source, 0, 0, data.w, data.h); data.ctx.globalCompositeOperation = 'destination-out'; var n = data.numParts; while (n--) data.parts[n] = 1; data.ratio = 0; data.complete = false; data.touchDown = false; } }, progress: function() { var $this = $(this), data = $this.data('eraser'); if (data) { return data.ratio/data.numParts; } return 0; } }; $.fn.eraser = function(method) { if (methods[method]) { return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); } else if (typeof method === 'object' || ! method) { return methods.init.apply(this, arguments); } else { $.error('Method ' + method + ' does not yet exist on jQuery.eraser'); } }; })(jQuery);