diff --git a/resources/mmv/img/blur.svg b/resources/mmv/img/blur.svg new file mode 100644 index 000000000..f422ea932 --- /dev/null +++ b/resources/mmv/img/blur.svg @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/resources/mmv/mmv.js b/resources/mmv/mmv.js index 5dc9cff9e..ac40e61e1 100755 --- a/resources/mmv/mmv.js +++ b/resources/mmv/mmv.js @@ -188,7 +188,7 @@ */ MMVP.setImage = function ( ui, thumbnail, image, imageWidths ) { // we downscale larger images but do not scale up smaller ones, that would look ugly - if ( thumbnail.width > imageWidths.screen ) { + if ( thumbnail.width > imageWidths.css ) { image.width = imageWidths.css; } @@ -205,7 +205,9 @@ var imageWidths, viewer = this, imagePromise, - metadataPromise; + metadataPromise, + start, + $initialImage = $( initialImage ); // FIXME dependency injection happens in completely random order and location, needs cleanup this.ui = this.lightbox.iface; @@ -223,7 +225,13 @@ this.lightbox.iface.empty(); } this.lightbox.iface.setupForLoad(); - this.lightbox.iface.showImage( image, initialImage ); + + // At this point we can't show the thumbnail because we don't + // know what size it should be. We still assign it to allow for + // size calculations in getCurrentImageWidths, which needs to know + // the aspect ratio + $initialImage.hide(); + this.lightbox.iface.showImage( image, $initialImage ); this.preloadImagesMetadata(); this.preloadThumbnails(); @@ -232,11 +240,23 @@ $( document.body ).addClass( 'mw-mlb-lightbox-open' ); imageWidths = this.ui.getCurrentImageWidths(); + + this.resetBlurredThumbnailStates(); + + start = $.now(); + imagePromise = this.fetchThumbnail( image.filePageTitle, imageWidths.real - ).done( function( thumbnail, image ) { - viewer.setImage( viewer.lightbox.iface, thumbnail, image, imageWidths ); - viewer.lightbox.iface.$imageDiv.removeClass( 'empty' ); + ).progress( function ( thumbnailInfoResponse, imageResponse ) { + if ( viewer.ui && viewer.ui.panel ) { + viewer.ui.panel.percent( imageResponse[ 1 ] ); + } + } ).done( function ( thumbnail, image ) { + viewer.displayRealThumbnail( thumbnail, image, imageWidths, $.now() - start ); + } ); + + this.imageInfoProvider.get( image.filePageTitle ).done( function ( imageInfo ) { + viewer.displayPlaceholderThumbnail( imageInfo, image, $initialImage, imageWidths ); } ); metadataPromise = this.fetchSizeIndependentLightboxInfo( @@ -278,6 +298,81 @@ } ); }; + /** + * Resets the cross-request states needed to handle the blurred thumbnail logic + */ + MMVP.resetBlurredThumbnailStates = function () { + this.realThumbnailShown = false; + this.blurredThumbnailShown = false; + }; + + /** + * Display the real, full-resolution, thumbnail that was fetched with fetchThumbnail + * @param {mw.mmv.model.Thumbnail} thumbnail + * @param {HTMLImageElement} image + * @param {mw.mmv.model.ThumbnailWidth} imageWidths + * @param {number} loadTime Time it took to load the thumbnail + */ + MMVP.displayRealThumbnail = function ( thumbnail, image, imageWidths, loadTime ) { + this.realThumbnailShown = true; + + this.setImage( this.lightbox.iface, thumbnail, image, imageWidths ); + + // We only animate unblur if the image wasn't loaded from the cache + // A load in < 10ms is considered to be a browser cache hit + // And of course we only unblur if there was a blur to begin with + if ( this.blurredThumbnailShown && loadTime > 10 ) { + this.lightbox.iface.unblur(); + } + }; + + /** + * Display the blurred thumbnail from the page + * @param {mw.mmv.model.Image} imageInfo + * @param {HTMLImageElement} image + * @param {jQuery} $initialImage The thumbnail from the page + * @param {mw.mmv.model.ThumbnailWidth} imageWidths + */ + MMVP.displayPlaceholderThumbnail = function ( imageInfo, image, $initialImage, imageWidths ) { + var ratio, + targetWidth, + targetHeight, + blowupFactor; + + // If the actual image has already been displayed, there's no point showing the blurry one + if ( this.realThumbnailShown ) { + return; + } + + // If the image is smaller than the screen we need to adjust the placeholder's size + if ( imageInfo.width < imageWidths.css ) { + targetWidth = imageInfo.width; + targetHeight = imageInfo.height; + } else { + ratio = $initialImage.height() / $initialImage.width(); + targetWidth = imageWidths.css; + targetHeight = Math.round( imageWidths.css * ratio ); + } + + blowupFactor = targetWidth / $initialImage.width(); + + // If the placeholder is too blown up, it's not worth showing it + if ( blowupFactor > 11 ) { + return; + } + + $initialImage.width( targetWidth ); + $initialImage.height( targetHeight ); + + // Only blur the placeholder if it's blown up significantly + if ( blowupFactor > 2 ) { + $initialImage.addClass( 'blurred' ); + this.blurredThumbnailShown = true; + } + + this.lightbox.iface.showImage( image, $initialImage.show() ); + }; + /** * Preload this many prev/next images to speed up navigation. * (E.g. preloadDistance = 3 means that the previous 3 and the next 3 images will be loaded.) @@ -502,6 +597,7 @@ imagePromise; thumbnailPromise = this.thumbnailInfoProvider.get( fileTitle, width ); + imagePromise = thumbnailPromise.then( function( thumbnail ) { return viewer.imageProvider.get( thumbnail.url ); } ); diff --git a/resources/mmv/mmv.less b/resources/mmv/mmv.less index 173565c45..2cb3c184e 100644 --- a/resources/mmv/mmv.less +++ b/resources/mmv/mmv.less @@ -21,6 +21,7 @@ @drag-height: 18px; @bottom-height: (@title-height + @drag-height); @metadata-background: rgb(251, 251, 251); +@progress-height: 3px; .mlb-image-wrapper, .mw-mlb-controls { @@ -307,6 +308,17 @@ body.mobile .mw-mlb-controls, display: none; } +.mlb-image img.blurred { + /** + * SVG is for Firefox + * Cannot be embedded because of the hash needed to access the filter inside the SVG + */ + filter: url(img/blur.svg#gaussian-blur); + filter: blur(3px); + -webkit-filter: blur(3px); + opacity: 0.8; +} + body.mw-mlb-lightbox-open { overflow-y: auto; } @@ -351,3 +363,20 @@ body.mw-mlb-lightbox-open #content { .mlb-post-image:hover .mw-mlb-drag-icon { opacity: 1; } + +.mw-mlb-progress { + width: 100%; + height: @progress-height; + background-color: rgb( 204, 204, 204 ); + margin-top: -@progress-height; +} + +.mw-mlb-progress.empty { + display: none; +} + +.mw-mlb-progress-percent { + width: 0; + height: @progress-height; + background-color: rgb( 0, 113, 188 ); +} diff --git a/resources/mmv/mmv.lightboxinterface.js b/resources/mmv/mmv.lightboxinterface.js index 430b3724c..40cc45270 100644 --- a/resources/mmv/mmv.lightboxinterface.js +++ b/resources/mmv/mmv.lightboxinterface.js @@ -107,15 +107,6 @@ this.panel = new mw.mmv.ui.MetadataPanel( this.$postDiv, this.$controlBar ); this.buttons = new mw.mmv.ui.Buttons( this.$imageWrapper, this.$closeButton, this.$fullscreenButton ); - this.initializeImage(); - }; - - /** - * Initialize the image element. - */ - LIP.initializeImage = function () { - this.$imageDiv - .addClass( 'empty' ); }; /** @@ -126,7 +117,6 @@ this.panel.empty(); - this.$imageDiv.addClass( 'empty' ); this.$imageDiv.empty(); if ( this.resizeListener ) { @@ -266,15 +256,17 @@ * Displays an already loaded image. * This is an alternative to load() when we have an image element with the image already loaded. * @param {mw.mmv.LightboxImage} image - * @param {HTMLImageElement } imageElement + * @param {jQuery} $imageElement */ - LIP.showImage = function( image, imageElement ) { + LIP.showImage = function( image, $imageElement ) { var iface = this; this.currentImage = image; - image.globalMaxWidth = imageElement.width; - image.globalMaxHeight = imageElement.height; - this.$image = $( imageElement ); + + image.globalMaxWidth = $imageElement.width(); + image.globalMaxHeight = $imageElement.height(); + + this.$image = $imageElement; this.autoResizeImage(); @@ -299,7 +291,7 @@ this.currentImage = image; image.getImageElement().done( function( image, ele ) { - iface.showImage( image, ele ); + iface.showImage( image, $( ele ) ); } ); }; @@ -348,9 +340,8 @@ var $image = $( imageEle ); - this.currentImage.src = imageEle.src; - this.$image.replaceWith( $image ); + this.currentImage.src = imageEle.src; this.$image = $image; this.$image.css( { @@ -449,6 +440,36 @@ } }; + /** + * Animates the image into focus + */ + LIP.unblur = function () { + var self = this, + animationLength = 300; + + // The blurred class has an opacity < 1. This animated the image to become fully opaque + this.$image + .addClass( 'blurred' ) + .animate( { opacity: 1.0 }, animationLength ); + + // During the same amount of time (animationLength) we animate a blur value from 3.0 to 0.0 + // We pass that value to an inline CSS Gaussian blur effect + $( { blur: 3.0 } ).animate( { blur: 0.0 }, { + duration: animationLength, + step: function ( step ) { + self.$image.css( { '-webkit-filter' : 'blur(' + step + 'px)', + 'filter' : 'blur(' + step + 'px)' } ); + }, + complete: function () { + // When the animation is complete, the blur value is 0 + // We apply empty CSS values to remove the inline styles applied by jQuery + // so that they don't get in the way of styles defined in CSS + self.$image.css( { '-webkit-filter' : '', 'opacity' : '' } ) + .removeClass( 'blurred' ); + } + } ); + }; + /** * Handle a fullscreen change event. * @param {jQuery.Event} e The fullscreen change event. diff --git a/resources/mmv/mmv.performance.js b/resources/mmv/mmv.performance.js index a6b6403b5..6b67630e8 100755 --- a/resources/mmv/mmv.performance.js +++ b/resources/mmv/mmv.performance.js @@ -57,10 +57,22 @@ try { request = this.newXHR(); + + request.onprogress = function ( e ) { + var percent; + + if ( e.lengthComputable ) { + percent = ( e.loaded / e.total ) * 100; + } + + deferred.notify( request.response, percent ); + }; + request.onreadystatechange = function () { var total = $.now() - start; if ( request.readyState === 4 ) { + deferred.notify( request.response, 100 ); deferred.resolve( request.response ); perf.recordEntryDelayed( type, total, url, request ); } diff --git a/resources/mmv/ui/mmv.ui.metadataPanel.js b/resources/mmv/ui/mmv.ui.metadataPanel.js index cdcab52c2..d8b0cd8d1 100644 --- a/resources/mmv/ui/mmv.ui.metadataPanel.js +++ b/resources/mmv/ui/mmv.ui.metadataPanel.js @@ -77,6 +77,8 @@ this.$dragIcon.removeClass( 'pointing-down' ); + this.$progress.addClass( 'empty' ); + this.fileReuse.empty(); }; @@ -90,6 +92,8 @@ MPP.initializeHeader = function () { var panel = this; + this.initializeProgress(); + this.$dragBar = $( '