websanova / wScratchPad

jQuery Scratch Pad Plugin
http://www.websanova.com/plugins/websanova/scratchpad
388 stars 176 forks source link

Black line draws after clicking 'Reset' #37

Open ShellyTan opened 10 years ago

ShellyTan commented 10 years ago

If I click on the image, drag off, and then click on reset, a black line begins to draw when I bring my cursor back over the image. Everything's fine if I stop 'scratching' while still in the image. But if I drag my cursor off the image and then click reset, it breaks. Clicking again stops the paintbrush effect.

screen shot 2014-07-23 at 7 44 55 pm

WORMSS commented 10 years ago

I can confirm this.

artyboy commented 10 years ago

Setting this.scratch = false; in the reset function fixed this issue for me.

croffasia commented 10 years ago
(function ($) {
  'use strict';

  function ScratchPad(el, options) {
    this.$el = $(el);
    this.options = options;

    this.init = false;
    this.enabled = true; 

    /* + Updated */
    this.e = null;

    this._generate();
  }

  ScratchPad.prototype = {
    _generate: function () {

      // Throw message if canvas is not supported.
      if (!$.support.canvas) {
        this.$el.append('Canvas is not supported in this browser.');
        return true;
      }

      // Setup canvas and context.
      this.canvas = document.createElement('canvas');
      this.ctx = this.canvas.getContext('2d');

      // Make sure it's at least relative.
      if (this.$el.css('position') === 'static') {
        this.$el.css('position', 'relative');
      }

      this.$img = $('<img src=""/>').attr('crossOrigin', '').css({position: 'absolute', width: '100%', height: '100%'});

      // Make sure we sett style width height here for elastic stretch
      // and better support for mobile if we are resizing the scratch pad.
      this.$scratchpad = $(this.canvas).css({position: 'absolute', width: '100%', height: '100%'});

      this.$scratchpad.bindMobileEvents();

      // Setup event handlers.
      this.$scratchpad
      .mousedown($.proxy(function (e) {

        // If disabled we just return true which menas
        // our our this.scratch will remain as false.
        if (!this.enabled) {
          return true;
        }

        /* + Updated */
        this.e = e;

        this.canvasOffset = $(this.canvas).offset();

        this.scratch = true;
        this._scratchFunc(e, 'Down');
      }, this))
      .mousemove($.proxy(function (e) {
        if (this.scratch) {

          /* + Updated */
          this.e = e;
          this._scratchFunc(e, 'Move');          
        }
      }, this))
      .mouseup($.proxy(function (e) {
        if (this.scratch) {
          this.scratch = false;

          /* + Updated */
          this.e = null;
          this._scratchFunc(e, 'Up');
        }
      }, this));

      // Run options
      this._setOptions();

      // Apepnd items
      this.$el.append(this.$img).append(this.$scratchpad);

      // Initialize and reset
      this.init = true;
      this.reset();
    },

    reset: function () {

      var _this = this,
          width = Math.ceil(this.$el.innerWidth()),
          height = Math.ceil(this.$el.innerHeight()),
          isRestoreScratch = false,
          devicePixelRatio = window.devicePixelRatio || 1;

      /* + Updated */
      this.scratch = false;

      // Set number of pixels required for getting scratch percentage.
      this.pixels = width * height;

      // We'll do a hard reset for the height here in case
      // we need to run this at differnt sizes.
      this.$scratchpad.attr('width', width).attr('height', height);

      this.canvas.setAttribute('width', width * devicePixelRatio);
      this.canvas.setAttribute('height', height * devicePixelRatio);
      this.ctx.scale(devicePixelRatio, devicePixelRatio);

      this.pixels = width * devicePixelRatio * height * devicePixelRatio;

      // Default to image hidden in case no bg or color is set.
      this.$img.hide();

      // Set bg.
      if (this.options.bg) {
        if (this.options.bg.charAt(0) === '#') {
          this.$el.css('backgroundColor', this.options.bg);
        }
        else {
          this.$el.css('backgroundColor', '');
          this.$img.attr('src', this.options.bg);
        }
      }

      // Set fg.
      if (this.options.fg) {
        if (this.options.fg.charAt(0) === '#') {
          this.ctx.fillStyle = this.options.fg;
          this.ctx.beginPath();
          this.ctx.rect(0, 0, width, height);
          this.ctx.fill();
          this.$img.show();
        }
        else {
          // Have to load image before we can use it.
          $(new Image())
          .attr('src', this.options.fg)
          .load(function () {
            _this.ctx.drawImage(this, 0, 0, width, height);
            _this.$img.show();
          });
        }
      }

      /* + Updated */
      if(this.e != null)
      {
        this.canvasOffset = $(this.canvas).offset();

        this.scratch = true;
        this._scratchFunc(this.e, 'Down');
      }
    },

    clear: function () {      
      this.ctx.clearRect(0, 0, Math.ceil(this.$el.innerWidth()), Math.ceil(this.$el.innerHeight()));
    },

    enable: function (enabled) {
      this.enabled = enabled === true ? true : false;
    },

    destroy: function () {
      this.$el.children().remove();
      $.removeData(this.$el, 'wScratchPad');
    },

    _setOptions: function () {
      var opt, func;

      for (opt in this.options) {
        this.options[opt] = this.$el.attr('data-' + opt) || this.options[opt];
        func = 'set' + opt.charAt(0).toUpperCase() + opt.substring(1);

        if (this[func]) {
          this[func](this.options[opt]);
        }
      }
    },

    setBg: function () {
      if (this.init) {
        this.reset();
      }
    },

    setFg: function () {
      this.setBg();
    },

    setCursor: function (cursor) {
      this.$el.css('cursor', cursor);
    },

    _scratchFunc: function (e, event) {
      e.pageX = Math.floor(e.pageX - this.canvasOffset.left);
      e.pageY = Math.floor(e.pageY - this.canvasOffset.top);

      this['_scratch' + event](e);

      if (this.options.realtime || event === 'Up') {
        if (this.options['scratch' + event]) {
            this.options['scratch' + event].apply(this, [e, this._scratchPercent()]);
        }
      }
    },

    _scratchPercent: function() {
      var hits = 0,
          imageData = this.ctx.getImageData(0,0, this.canvas.width, this.canvas.height);

      for (var i=0, ii=imageData.data.length; i<ii; i=i+4) {
        if (imageData.data[i] === 0 && imageData.data[i+1] === 0 && imageData.data[i+2] === 0 && imageData.data[i+3] === 0) {
          hits++;
        }
      }

      return (hits / this.pixels) * 100;
    },

    _scratchDown: function (e) {
      this.ctx.globalCompositeOperation = 'destination-out';
      this.ctx.lineJoin = 'round';
      this.ctx.lineCap = 'round';
      this.ctx.strokeStyle = this.options.color;
      this.ctx.lineWidth = this.options.size;

      //draw single dot in case of a click without a move
      this.ctx.beginPath();
      this.ctx.arc(e.pageX, e.pageY, this.options.size/2, 0, Math.PI*2, true);
      this.ctx.closePath();
      this.ctx.fill();

      //start the path for a drag
      this.ctx.beginPath();
      this.ctx.moveTo(e.pageX, e.pageY);
    },

    _scratchMove: function (e) {
      this.ctx.lineTo(e.pageX, e.pageY);
      this.ctx.stroke();
    },

    _scratchUp: function () {
      this.ctx.closePath();
    }
  };

  $.support.canvas = (document.createElement('canvas')).getContext;

  $.fn.wScratchPad = function (options, value) {
    function get() {
      var wScratchPad = $.data(this, 'wScratchPad');

      if (!wScratchPad) {
        wScratchPad = new ScratchPad(this, $.extend(true, {}, options));
        $.data(this, 'wScratchPad', wScratchPad);
      }

      return wScratchPad;
    }

    if (typeof options === 'string') {
      var wScratchPad,
          values = [],
          func = (value !== undefined ? 'set' : 'get') + options.charAt(0).toUpperCase() + options.substring(1),

          setOpt = function () {
            if (wScratchPad.options[options]) { wScratchPad.options[options] = value; }
            if (wScratchPad[func]) { wScratchPad[func].apply(wScratchPad, [value]); }
          },

          getOpt = function () {
            if (wScratchPad[func]) { return wScratchPad[func].apply(wScratchPad, [value]); }
            else if (wScratchPad.options[options]) { return wScratchPad.options[options]; }
            else { return undefined; }
          },

          runOpt = function () {
            wScratchPad = $.data(this, 'wScratchPad');

            if (wScratchPad) {
              if (wScratchPad[options]) { wScratchPad[options].apply(wScratchPad, [value]); }
              else if (value !== undefined) { setOpt(); }
              else {  values.push(getOpt()); }
            }
          };

      this.each(runOpt);

      return values.length ? (values.length === 1 ? values[0] : values) : this;
    }

    options = $.extend({}, $.fn.wScratchPad.defaults, options);

    return this.each(get);
  };

  $.fn.wScratchPad.defaults = {
    size        : 5,          // The size of the brush/scratch.
    bg          : '#cacaca',  // Background (image path or hex color).
    fg          : '#6699ff',  // Foreground (image path or hex color).
    realtime    : true,       // Calculates percentage in realitime
    scratchDown : null,       // Set scratchDown callback.
    scratchUp   : null,       // Set scratchUp callback.
    scratchMove : null,       // Set scratcMove callback.
    cursor      : 'crosshair' // Set cursor.
  };

  $.fn.bindMobileEvents = function () {
    $(this).on('touchstart touchmove touchend touchcancel', function (event) {
      var touches = (event.changedTouches || event.originalEvent.targetTouches),
          first = touches[0],
          type = '';

      switch (event.type) {
      case 'touchstart':
        type = 'mousedown';
        break;
      case 'touchmove':
        type = 'mousemove';
        event.preventDefault();
        break;
      case 'touchend':
        type = 'mouseup';
        break;
      default:
        return;
      }

      var simulatedEvent = document.createEvent('MouseEvent'); 

      simulatedEvent.initMouseEvent(
        type, true, true, window, 1, 
        first.screenX, first.screenY, first.clientX, first.clientY, 
        false, false, false, false, 0/*left*/, null
      );

      first.target.dispatchEvent(simulatedEvent);
    });
  };
})(jQuery);

When triggered MouseEvent Move need to replace the background and reset without releasing MouseEvent Up. Small fix:-) See comment /* + Updated */

Use:

// After first click at slot need loading backgroundimage
scratchDown: function(e, percent){
if(needLoad == true){
   this.options.bg = '/images/smiley.jpg';
   this.reset();
}
}

Sorry for my English :-)

WORMSS commented 10 years ago

@PoluosmakAndrew Would this not have been better as a Pull request that referenced this issue?