/* * Copyright 2014 Tom * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ (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: clipped to [0..1], default 0 * - handleClass: 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 = $('').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);