From 8c4fcc9ca9289b54f3dd77155ff0b3888a3847e7 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Wed, 4 Nov 2015 17:04:50 +0200 Subject: [PATCH] tile edge smoothing at high zoom - #755 --- src/drawer.js | 24 ++++++++++++++---- src/openseadragon.js | 6 +++++ src/tile.js | 59 +++++++++++++++++++++++++++++++++++--------- src/tiledimage.js | 44 ++++++++++++++++++++++----------- src/viewer.js | 3 ++- 5 files changed, 104 insertions(+), 32 deletions(-) diff --git a/src/drawer.js b/src/drawer.js index 9b74b981..5666370d 100644 --- a/src/drawer.js +++ b/src/drawer.js @@ -290,8 +290,9 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ * drawingHandler({context, tile, rendered}) * @param {Boolean} useSketch - Whether to use the sketch canvas or not. * where rendered is the context with the pre-drawn image. + * @param {Float} scale - Apply a scale to tile position and size */ - drawTile: function( tile, drawingHandler, useSketch ) { + drawTile: function( tile, drawingHandler, useSketch, scale ) { $.console.assert(tile, '[Drawer.drawTile] tile is required'); $.console.assert(drawingHandler, '[Drawer.drawTile] drawingHandler is required'); @@ -301,10 +302,10 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ // specifically, don't save,rotate,restore every time we draw a tile if( this.viewport.degrees !== 0 ) { this._offsetForRotation( tile, this.viewport.degrees, useSketch ); - tile.drawCanvas( context, drawingHandler ); + tile.drawCanvas( context, drawingHandler, scale ); this._restoreRotationChanges( tile, useSketch ); } else { - tile.drawCanvas( context, drawingHandler ); + tile.drawCanvas( context, drawingHandler, scale ); } } else { tile.drawHTML( this.canvas ); @@ -371,16 +372,29 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ /** * Blends the sketch canvas in the main canvas. * @param {Float} opacity The opacity of the blending. + * @param {Float} sketchScale The scale at which tiles were drawn on the sketch. Default is 1. + * Use sketchScale to draw at a lower scale and then enlarge onto the main canvas. * @returns {undefined} */ - blendSketch: function(opacity) { + blendSketch: function(opacity, sketchScale) { if (!this.useCanvas || !this.sketchCanvas) { return; } + sketchScale = sketchScale || 1; this.context.save(); this.context.globalAlpha = opacity; - this.context.drawImage(this.sketchCanvas, 0, 0); + this.context.drawImage( + this.sketchCanvas, + 0, + 0, + this.sketchCanvas.width * sketchScale, + this.sketchCanvas.height * sketchScale, + 0, + 0, + this.canvas.width, + this.canvas.height + ); this.context.restore(); }, diff --git a/src/openseadragon.js b/src/openseadragon.js index 6b36327b..df4dd55f 100644 --- a/src/openseadragon.js +++ b/src/openseadragon.js @@ -249,6 +249,11 @@ * image though it is less effective visually if the HTML5 Canvas is not * availble on the viewing device. * + * @property {Number} [smoothTileEdgesMinZoom=1.1] + * A zoom percentage ( expressed as a number between 0 and 1 ) of the highest + * resolution level. When zoomed in beyond this value alternative compositing will + * be used to smooth out the edges between tiles. This WILL have a performance impact. + * * @property {Boolean} [autoResize=true] * Set to false to prevent polling for viewer size changes. Useful for providing custom resize behavior. * @@ -1000,6 +1005,7 @@ if (typeof define === 'function' && define.amd) { immediateRender: false, minZoomImageRatio: 0.9, //-> closer to 0 allows zoom out to infinity maxZoomPixelRatio: 1.1, //-> higher allows 'over zoom' into pixels + smoothTileEdgesMinZoom: 1.1, //-> higher than maxZoomPixelRatio disables it pixelsPerWheelLine: 40, autoResize: true, preserveImageSizeOnResize: false, // requires autoResize=true diff --git a/src/tile.js b/src/tile.js index ad018ba7..9a7fde55 100644 --- a/src/tile.js +++ b/src/tile.js @@ -240,11 +240,12 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{ * @param {Function} drawingHandler - Method for firing the drawing event. * drawingHandler({context, tile, rendered}) * where rendered is the context with the pre-drawn image. + * @param {Number} scale - Apply a scale to position and size */ - drawCanvas: function( context, drawingHandler ) { + drawCanvas: function( context, drawingHandler, scale ) { - var position = this.position, - size = this.size, + var position = this.position.times($.pixelDensityRatio), + size = this.size.times($.pixelDensityRatio), rendered; if (!this.cacheImageRecord) { @@ -277,10 +278,10 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{ //clearing only the inside of the rectangle occupied //by the png prevents edge flikering context.clearRect( - (position.x * $.pixelDensityRatio)+1, - (position.y * $.pixelDensityRatio)+1, - (size.x * $.pixelDensityRatio)-2, - (size.y * $.pixelDensityRatio)-2 + position.x + 1, + position.y + 1, + size.x - 2, + size.y - 2 ); } @@ -289,16 +290,52 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{ // changes as we are rendering the image drawingHandler({context: context, tile: this, rendered: rendered}); + if (typeof scale === 'number' && scale !== 1) { + // draw tile at a different scale + position = position.times(scale); + size = size.times(scale); + + if (scale < 1 && $.Browser.vendor == $.BROWSERS.FIREFOX) { + // In firefox edges are very visible because there seems to be + // empty space between tiles caused by float coordinates. + // Adding partial overlap fixes this. + // These will be covered by the top and left tiles. + context.drawImage( // duplicate first column to the left + rendered.canvas, + 0, + 0, + 1, + rendered.canvas.height, + Math.floor(position.x), + position.y, + 1, + size.y + ); + context.drawImage( // duplicate first row up + rendered.canvas, + 0, + 0, + rendered.canvas.width, + 1, + position.x, + Math.floor(position.y), + size.x, + 1 + ); + } + } + + // context.globalCompositeOperation = 'source-out'; context.drawImage( rendered.canvas, 0, 0, rendered.canvas.width, rendered.canvas.height, - position.x * $.pixelDensityRatio, - position.y * $.pixelDensityRatio, - size.x * $.pixelDensityRatio, - size.y * $.pixelDensityRatio + position.x, + position.y, + size.x, + size.y ); context.restore(); diff --git a/src/tiledimage.js b/src/tiledimage.js index 1731d472..815d052c 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -133,19 +133,20 @@ $.TiledImage = function( options ) { _hasOpaqueTile: false, // Do we have even one fully opaque tile? //configurable settings - springStiffness: $.DEFAULT_SETTINGS.springStiffness, - animationTime: $.DEFAULT_SETTINGS.animationTime, - minZoomImageRatio: $.DEFAULT_SETTINGS.minZoomImageRatio, - wrapHorizontal: $.DEFAULT_SETTINGS.wrapHorizontal, - wrapVertical: $.DEFAULT_SETTINGS.wrapVertical, - immediateRender: $.DEFAULT_SETTINGS.immediateRender, - blendTime: $.DEFAULT_SETTINGS.blendTime, - alwaysBlend: $.DEFAULT_SETTINGS.alwaysBlend, - minPixelRatio: $.DEFAULT_SETTINGS.minPixelRatio, - debugMode: $.DEFAULT_SETTINGS.debugMode, - crossOriginPolicy: $.DEFAULT_SETTINGS.crossOriginPolicy, - placeholderFillStyle: $.DEFAULT_SETTINGS.placeholderFillStyle, - opacity: $.DEFAULT_SETTINGS.opacity + springStiffness: $.DEFAULT_SETTINGS.springStiffness, + animationTime: $.DEFAULT_SETTINGS.animationTime, + minZoomImageRatio: $.DEFAULT_SETTINGS.minZoomImageRatio, + wrapHorizontal: $.DEFAULT_SETTINGS.wrapHorizontal, + wrapVertical: $.DEFAULT_SETTINGS.wrapVertical, + immediateRender: $.DEFAULT_SETTINGS.immediateRender, + blendTime: $.DEFAULT_SETTINGS.blendTime, + alwaysBlend: $.DEFAULT_SETTINGS.alwaysBlend, + minPixelRatio: $.DEFAULT_SETTINGS.minPixelRatio, + smoothTileEdgesMinZoom: $.DEFAULT_SETTINGS.smoothTileEdgesMinZoom, + debugMode: $.DEFAULT_SETTINGS.debugMode, + crossOriginPolicy: $.DEFAULT_SETTINGS.crossOriginPolicy, + placeholderFillStyle: $.DEFAULT_SETTINGS.placeholderFillStyle, + opacity: $.DEFAULT_SETTINGS.opacity }, options ); @@ -1302,6 +1303,19 @@ function drawTiles( tiledImage, lastDrawn ) { return; } var useSketch = tiledImage.opacity < 1; + var sketchScale = 1; + + var zoom = tiledImage.viewport.getZoom(); + var imageZoom = tiledImage.viewportToImageZoom(zoom); + if ( imageZoom > tiledImage.smoothTileEdgesMinZoom ) { + // When zoomed in a lot (>100%) the tile edges are visible. + // So we have to composite them at ~100% and scale them up together. + useSketch = true; + // Compositing at 100% is not precise and causes weird twithing. + // So we composite at 101% zoom + sketchScale = 1.01 / imageZoom; + } + if ( useSketch ) { tiledImage._drawer._clear( true ); } @@ -1333,7 +1347,7 @@ function drawTiles( tiledImage, lastDrawn ) { for ( i = lastDrawn.length - 1; i >= 0; i-- ) { tile = lastDrawn[ i ]; - tiledImage._drawer.drawTile( tile, tiledImage._drawingHandler, useSketch ); + tiledImage._drawer.drawTile( tile, tiledImage._drawingHandler, useSketch, sketchScale ); tile.beingDrawn = true; if( tiledImage.viewer ){ @@ -1360,7 +1374,7 @@ function drawTiles( tiledImage, lastDrawn ) { } if ( useSketch ) { - tiledImage._drawer.blendSketch( tiledImage.opacity ); + tiledImage._drawer.blendSketch( tiledImage.opacity, sketchScale ); } drawDebugInfo( tiledImage, lastDrawn ); } diff --git a/src/viewer.js b/src/viewer.js index c66556f1..0851f0e6 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -604,7 +604,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, var originalSuccess = options.success; options.success = function(event) { successes++; - + // TODO: now that options has other things besides tileSource, the overlays // should probably be at the options level, not the tileSource level. if (options.tileSource.overlays) { @@ -1342,6 +1342,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, blendTime: _this.blendTime, alwaysBlend: _this.alwaysBlend, minPixelRatio: _this.minPixelRatio, + smoothTileEdgesMinZoom: _this.smoothTileEdgesMinZoom, crossOriginPolicy: _this.crossOriginPolicy, debugMode: _this.debugMode });