Paul Martin
2016-04-16 eecaad8b8e2c447429c31a01d49260ddd6b4ee03
src/main/java/com/gitblit/wicket/pages/scripts/imgdiff.js
@@ -13,11 +13,251 @@
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
jQuery(function () {
   // Runs on jQuery's document.ready and sets up the scroll event handlers for all image diffs.
   jQuery(".imgdiff-slider").scroll(function() {
      var w = 1.0 - (this.scrollLeft / (this.scrollWidth - (this.clientWidth || this.offsetWidth)));
      // We encode the target img id in the slider's id: slider-imgdiffNNN.
      jQuery('#' + this.id.substr(this.id.indexOf('-') + 1)).css("opacity", w);
(function($) {
/**
 * Sets up elem as a slider; returns an access object. Elem must be positioned!
 * Note that the element may contain other elements; this is used for instance
 * for the image diff overlay slider.
 *
 * The styling of the slider is to be done in CSS. Currently recognized options:
 * - initial: <float> clipped to [0..1], default 0
 * - handleClass: <string> to assign to the handle span element created.
 * If no handleClass is specified, a very plain default style is assigned.
 */
function rangeSlider(elem, options) {
   options = $.extend({ initial : 0 }, options || {});
   options.initial = Math.min(1.0, Math.max(0, options.initial));
   var $elem = $(elem);
   var $handle = $('<span></span>').css({ position: 'absolute', left: 0, cursor: 'ew-resize' });
   var $root = $(document.documentElement);
   var $doc = $(document);
   var lastRatio = options.initial;
   /** Mousemove event handler to track the mouse and move the slider. Generates slider:pos events. */
   function track(e) {
      var pos = $elem.offset().left;
      var width = $elem.innerWidth();
      var handleWidth = $handle.outerWidth(false);
      var range = width - handleWidth;
      if (range <= 0) return;
      var delta = Math.min(range, Math.max (0, e.pageX - pos - handleWidth / 2));
      lastRatio = delta / range;
      $handle.css('left', "" + (delta * 100 / width) + '%');
      $elem.trigger('slider:pos', { ratio: lastRatio, handle: $handle[0] });
   }
   /** Mouseup event handler to stop mouse tracking. */
   function end(e) {
      $doc.off('mousemove', track);
      $doc.off('mouseup', end);
      $root.removeClass('no-select');
   }
    /** Snaps the slider to the given ratio and generates a slider:pos event with the new ratio. */
   function setTo(ratio) {
      var w = $elem.innerWidth();
      if (w <= 0 || $elem.is(':hidden')) return;
      lastRatio = Math.min( 1.0, Math.max(0, ratio));
      $handle.css('left', "" + Math.max(0, 100 * (lastRatio * (w - $handle.outerWidth(false))) / w) + '%');
      $elem.trigger('slider:pos', { ratio: lastRatio, handle: $handle[0] });
   }
   /**
    * Moves the slider to the given ratio, clipped to [0..1], in duration milliseconds.
    * Generates slider:pos events during the animation. If duration <= 30, same as setTo.
    * Default duration is 500ms. If a callback is given, it's called once the animation
    * has completed.
    */
   function moveTo(ratio, duration, callback) {
      ratio = Math.min(1.0, Math.max(0, ratio));
      if (ratio === lastRatio) {
         if (typeof callback == 'function') callback();
         return;
      }
      if (typeof duration == 'undefined') duration = 500;
      if (duration <= 30) {
          // Cinema is 24 or 48 frames/sec, so 20-40ms per frame. Makes no sense to animate for such a short duration.
         setTo(ratio);
         if (typeof callback == 'function') callback();
      } else {
         var target = ratio * ($elem.innerWidth() - $handle.outerWidth(false));
         if (ratio > lastRatio) target--; else target++;
         $handle.stop().animate({left: target},
            { 'duration' : duration,
              'step' : function() {
                  lastRatio = Math.min(1.0, Math.max(0, $handle.position().left / ($elem.innerWidth() - $handle.outerWidth(false))));
                  $elem.trigger('slider:pos', { ratio : lastRatio, handle : $handle[0] });
               },
              'complete' : function() { setTo(ratio); if (typeof callback == 'function') callback(); } // Ensure we have again a % value
            }
         );
      }
   }
   /**
    * As moveTo, but determines an appropriate duration in the range [0..maxDuration] on its own,
    * depending on the distance the handle would move. If no maxDuration is given it defaults
    * to 1500ms.
    */
   function moveAuto(ratio, maxDuration, callback) {
      if (typeof maxDuration == 'undefined') maxDuration = 1500;
      var delta = ratio - lastRatio;
      if (delta < 0) delta = -delta;
      var speed = $elem.innerWidth() * delta * 2;
      if (speed > maxDuration) speed = maxDuration;
      moveTo(ratio, speed, callback);
   }
   /** Returns the current ratio. */
   function getValue() {
      return lastRatio;
   }
   $elem.append($handle);
   if (options.handleClass) {
      $handle.addClass(options.handleClass);
   } else { // Provide a default style so that it is at least visible
      $handle.css({ width: '10px', height: '10px', background: 'white', border: '1px solid black' });
   }
   if (options.initial) setTo(options.initial);
   /** Install mousedown handler to start mouse tracking. */
   $handle.on('mousedown', function(e) {
      $root.addClass('no-select');
      $doc.on('mousemove', track);
      $doc.on('mouseup', end);
      e.stopPropagation();
      e.preventDefault();
   });
});
   return { setRatio: setTo, moveRatio: moveTo, 'moveAuto': moveAuto, getRatio: getValue, handle: $handle[0] };
}
function setup() {
   $('.imgdiff-container').each(function() {
      var $this = $(this);
      var $overlaySlider = $this.find('.imgdiff-ovr-slider').first();
      var $opacitySlider = $this.find('.imgdiff-opa-slider').first();
      var overlayAccess = rangeSlider($overlaySlider, {handleClass: 'imgdiff-ovr-handle'});
      var opacityAccess = rangeSlider($opacitySlider, {handleClass: 'imgdiff-opa-handle'});
      var $img = $('#' + this.id.substr(this.id.indexOf('-')+1)); // Here we change opacity
      var $div = $img.parent(); // This controls visibility: here we change width.
      var blinking = false;
      $overlaySlider.on('slider:pos', function(e, data) {
         var pos = $(data.handle).offset().left;
         var imgLeft = $img.offset().left; // Global
         var imgW = $img.outerWidth(true);
         var imgOff = $img.position().left; // From left edge of $div
         if (pos <= imgLeft) {
            $div.width(0);
         } else if (pos <= imgLeft + imgW) {
            $div.width(pos - imgLeft + imgOff);
         } else if ($div.width() < imgW + imgOff) {
            $div.width(imgW + imgOff);
         }
      });
      $overlaySlider.css('cursor', 'pointer');
      $overlaySlider.on('mousedown', function(e) {
         var newRatio = (e.pageX - $overlaySlider.offset().left) / $overlaySlider.innerWidth();
         var oldRatio = overlayAccess.getRatio();
         if (newRatio !== oldRatio) {
            overlayAccess.moveAuto(newRatio);
         }
      });
      var autoShowing = false;
      $opacitySlider.on('slider:pos', function(e, data) {
         if ($div.width() <= 0 && !blinking) {
            // Make old image visible in a nice way, *then* adjust opacity
            autoShowing = true;
            overlayAccess.moveAuto(1.0, 500, function() {
               $img.stop().animate(
                  {opacity: 1.0 - opacityAccess.getRatio()},
                  {duration: 400,
                   complete: function () {
                     // In case the opacity handle was moved while we were trying to catch up
                     $img.css('opacity', 1.0 - opacityAccess.getRatio());
                     autoShowing = false;
                   }
                  }
               );
            });
         } else if (!autoShowing) {
            $img.css('opacity', 1.0 - data.ratio);
         }
      });
      $opacitySlider.on('click', function(e) {
         var newRatio = (e.pageX - $opacitySlider.offset().left) / $opacitySlider.innerWidth();
         var oldRatio = opacityAccess.getRatio();
         if (newRatio !== oldRatio) {
            if ($div.width() <= 0) {
               overlayAccess.moveRatio(1.0, 500, function() {opacityAccess.moveAuto(newRatio);}); // Make old image visible in a nice way
            } else {
               opacityAccess.moveAuto(newRatio)
            }
         }
         e.preventDefault();
      });
      // Blinking before and after images is a good way for the human eye to catch differences.
      var $blinker = $this.find('.imgdiff-blink');
      var initialOpacity = null;
      $blinker.on('click', function(e) {
         if (blinking) {
            window.clearTimeout(blinking);
            $blinker.children('img').first().css('border', '1px solid transparent');
            opacityAccess.setRatio(initialOpacity);
            blinking = null;
         } else {
            $blinker.children('img').first().css('border', '1px solid #AAA');
            initialOpacity = opacityAccess.getRatio();
            var currentOpacity = 1.0;
            function blink() {
               opacityAccess.setRatio(currentOpacity);
               currentOpacity = 1.0 - currentOpacity;
               // Keep frequeny below 2Hz (i.e., delay above 500ms)
               blinking = window.setTimeout(blink, 600);
            }
            if ($div.width() <= 0) {
               overlayAccess.moveRatio(1.0, 500, blink);
            } else {
               blink();
            }
         }
         e.preventDefault();
      });
      // Subtracting before and after images is another good way to detect differences. Result will be
      // black where identical.
      if (typeof $img[0].style.mixBlendMode != 'undefined') {
         // Feature test: does the browser support the mix-blend-mode CSS property from the Compositing
         // and Blending Level 1 spec (http://dev.w3.org/fxtf/compositing-1/#mix-blend-mode )?
         // As of 2014-11, only Firefox >= 32 and Safari >= 7.1 support this. Other browsers will have to
         // make do with the blink comparator only.
         var $sub = $this.find('.imgdiff-subtract');
         $sub.css('display', 'inline-block');
         $sub.on('click', function (e) {
            var curr = $img.css('mix-blend-mode');
            if (curr != 'difference') {
               curr = 'difference';
               $sub.children('img').first().css('border', '1px solid #AAA');
               if ($div.width() <= 0) overlayAccess.moveRatio(1.0, 500);
               opacityAccess.setRatio(0);
            } else {
               curr = 'normal';
               $sub.children('img').first().css('border', '1px solid transparent');
            }
            $img.css('mix-blend-mode', curr);
            e.preventDefault();
         });
      }
   });
}
$(setup); // Run on jQuery's dom-ready
})(jQuery);