finished many implementation details and demo

This commit is contained in:
Tom 2023-03-05 16:08:32 -05:00
parent f9ab63944b
commit 5328761877
12 changed files with 2252 additions and 1676 deletions

View File

@ -57,6 +57,7 @@ module.exports = function(grunt) {
"src/imageloader.js",
"src/tile.js",
"src/overlay.js",
"src/drawerbase.js",
"src/drawer.js",
"src/viewport.js",
"src/tiledimage.js",

View File

@ -44,7 +44,9 @@
* @param {Element} options.element - Parent element.
* @param {Number} [options.debugGridColor] - See debugGridColor in {@link OpenSeadragon.Options} for details.
*/
$.Drawer = function( options ) {
$.Drawer = function(options) {
$.DrawerBase.call(this, options);
$.console.assert( options.viewer, "[Drawer] options.viewer is required" );
@ -138,51 +140,27 @@ $.Drawer = function( options ) {
// Image smoothing for canvas rendering (only if canvas is used).
// Canvas default is "true", so this will only be changed if user specified "false".
this._imageSmoothingEnabled = true;
};
/** @lends OpenSeadragon.Drawer.prototype */
$.Drawer.prototype = {
$.extend( $.Drawer.prototype, $.DrawerBase.prototype, /** @lends OpenSeadragon.Drawer.prototype */ {
/**
* This function converts the given point from to the drawer coordinate by
* multiplying it with the pixel density.
* This function does not take rotation into account, thus assuming provided
* point is at 0 degree.
* @param {OpenSeadragon.Point} point - the pixel point to convert
* @returns {OpenSeadragon.Point} Point in drawer coordinate system.
* Draws the TiledImage to its Drawer.
*/
viewportCoordToDrawerCoord: function(point) {
var vpPoint = this.viewport.pixelFromPointNoRotate(point, true);
return new $.Point(
vpPoint.x * $.pixelDensityRatio,
vpPoint.y * $.pixelDensityRatio
);
},
/**
* This function will create multiple polygon paths on the drawing context by provided polygons,
* then clip the context to the paths.
* @param {OpenSeadragon.Point[][]} polygons - an array of polygons. A polygon is an array of OpenSeadragon.Point
* @param {Boolean} useSketch - Whether to use the sketch canvas or not.
*/
clipWithPolygons: function (polygons, useSketch) {
if (!this.useCanvas) {
return;
draw: function(tiledImage) {
if (tiledImage.opacity !== 0 || tiledImage._preload) {
tiledImage._midDraw = true;
this._updateViewport(tiledImage);
tiledImage._midDraw = false;
}
// Images with opacity 0 should not need to be drawn in future. this._needsDraw = false is set in this._updateViewport() for other images.
else {
tiledImage._needsDraw = false;
}
var context = this._getContext(useSketch);
context.beginPath();
polygons.forEach(function (polygon) {
polygon.forEach(function (coord, i) {
context[i === 0 ? 'moveTo' : 'lineTo'](coord.x, coord.y);
});
});
context.clip();
},
/**
* @returns {Boolean} True if rotation is supported.
*/
@ -237,6 +215,419 @@ $.Drawer.prototype = {
}
},
/* Methods from TiledImage */
/**
* @private
* @inner
* Pretty much every other line in this needs to be documented so it's clear
* how each piece of this routine contributes to the drawing process. That's
* why there are so many TODO's inside this function.
*/
_updateViewport: function(tiledImage) {
var _this = this;
tiledImage._needsDraw = false;
tiledImage._tilesLoading = 0;
tiledImage.loadingCoverage = {};
// Reset tile's internal drawn state
while (tiledImage.lastDrawn.length > 0) {
var tile = tiledImage.lastDrawn.pop();
tile.beingDrawn = false;
}
var drawArea = tiledImage.getDrawArea();
if(!drawArea){
return;
}
function updateTile(info){
var tile = info.tile;
if(tile && tile.loaded){
var needsDraw = _this._blendTile(
tiledImage,
tile,
tile.x,
tile.y,
info.level,
info.levelOpacity,
info.currentTime
);
if(needsDraw){
tiledImage._needsDraw = true;
}
}
}
var infoArray = tiledImage.getTileInfoForDrawing();
infoArray.forEach(updateTile);
this._drawTiles(tiledImage, tiledImage.lastDrawn);
},
/**
* @private
* @inner
* Updates the opacity of a tile according to the time it has been on screen
* to perform a fade-in.
* Updates coverage once a tile is fully opaque.
* Returns whether the fade-in has completed.
*
* @param {OpenSeadragon.Tile} tile
* @param {Number} x
* @param {Number} y
* @param {Number} level
* @param {Number} levelOpacity
* @param {Number} currentTime
* @returns {Boolean}
*/
_blendTile: function( tiledImage, tile, x, y, level, levelOpacity, currentTime ){
var blendTimeMillis = 1000 * tiledImage.blendTime,
deltaTime,
opacity;
if ( !tile.blendStart ) {
tile.blendStart = currentTime;
}
deltaTime = currentTime - tile.blendStart;
opacity = blendTimeMillis ? Math.min( 1, deltaTime / ( blendTimeMillis ) ) : 1;
if ( tiledImage.alwaysBlend ) {
opacity *= levelOpacity;
}
tile.opacity = opacity;
tiledImage.lastDrawn.push( tile );
if ( opacity === 1 ) {
tiledImage._setCoverage( tiledImage.coverage, level, x, y, true );
tiledImage._hasOpaqueTile = true;
} else if ( deltaTime < blendTimeMillis ) {
return true;
}
return false;
},
/**
* @private
* @inner
* Draws a TiledImage.
*
*/
_drawTiles: function( tiledImage ) {
var lastDrawn = tiledImage.lastDrawn;
if (tiledImage.opacity === 0 || (lastDrawn.length === 0 && !tiledImage.placeholderFillStyle)) {
return;
}
var tile = lastDrawn[0];
var useSketch;
if (tile) {
useSketch = tiledImage.opacity < 1 ||
(tiledImage.compositeOperation && tiledImage.compositeOperation !== 'source-over') ||
(!tiledImage._isBottomItem() &&
tiledImage.source.hasTransparency(tile.context2D, tile.getUrl(), tile.ajaxHeaders, tile.postData));
}
var sketchScale;
var sketchTranslate;
var zoom = this.viewport.getZoom(true);
var imageZoom = tiledImage.viewportToImageZoom(zoom);
if (lastDrawn.length > 1 &&
imageZoom > tiledImage.smoothTileEdgesMinZoom &&
!tiledImage.iOSDevice &&
tiledImage.getRotation(true) % 360 === 0 && // TODO: support tile edge smoothing with tiled image rotation.
$.supportsCanvas && this.viewer.useCanvas) {
// When zoomed in a lot (>100%) the tile edges are visible.
// So we have to composite them at ~100% and scale them up together.
// Note: Disabled on iOS devices per default as it causes a native crash
useSketch = true;
sketchScale = tile.getScaleForEdgeSmoothing();
sketchTranslate = tile.getTranslationForEdgeSmoothing(sketchScale,
this.getCanvasSize(false),
this.getCanvasSize(true));
}
var bounds;
if (useSketch) {
if (!sketchScale) {
// Except when edge smoothing, we only clean the part of the
// sketch canvas we are going to use for performance reasons.
bounds = this.viewport.viewportToViewerElementRectangle(
tiledImage.getClippedBounds(true))
.getIntegerBoundingBox();
if(this.viewer.viewport.getFlip()) {
if (this.viewport.getRotation(true) % 360 !== 0 ||
tiledImage.getRotation(true) % 360 !== 0) {
bounds.x = this.viewer.container.clientWidth - (bounds.x + bounds.width);
}
}
bounds = bounds.times($.pixelDensityRatio);
}
this._clear(true, bounds);
}
// When scaling, we must rotate only when blending the sketch canvas to
// avoid interpolation
if (!sketchScale) {
if (this.viewport.getRotation(true) % 360 !== 0) {
this._offsetForRotation({
degrees: this.viewport.getRotation(true),
useSketch: useSketch
});
}
if (tiledImage.getRotation(true) % 360 !== 0) {
this._offsetForRotation({
degrees: tiledImage.getRotation(true),
point: this.viewport.pixelFromPointNoRotate(
tiledImage._getRotationPoint(true), true),
useSketch: useSketch
});
}
if (this.viewport.getRotation(true) % 360 === 0 &&
tiledImage.getRotation(true) % 360 === 0) {
if(this.viewer.viewport.getFlip()) {
this._flip();
}
}
}
var usedClip = false;
if ( tiledImage._clip ) {
this.saveContext(useSketch);
var box = tiledImage.imageToViewportRectangle(tiledImage._clip, true);
box = box.rotate(-tiledImage.getRotation(true), tiledImage._getRotationPoint(true));
var clipRect = this.viewportToDrawerRectangle(box);
if (sketchScale) {
clipRect = clipRect.times(sketchScale);
}
if (sketchTranslate) {
clipRect = clipRect.translate(sketchTranslate);
}
this.setClip(clipRect, useSketch);
usedClip = true;
}
if (tiledImage._croppingPolygons) {
var self = this;
this.saveContext(useSketch);
try {
var polygons = tiledImage._croppingPolygons.map(function (polygon) {
return polygon.map(function (coord) {
var point = tiledImage
.imageToViewportCoordinates(coord.x, coord.y, true)
.rotate(-tiledImage.getRotation(true), tiledImage._getRotationPoint(true));
var clipPoint = self.viewportCoordToDrawerCoord(point);
if (sketchScale) {
clipPoint = clipPoint.times(sketchScale);
}
return clipPoint;
});
});
this.clipWithPolygons(polygons, useSketch);
} catch (e) {
$.console.error(e);
}
usedClip = true;
}
if ( tiledImage.placeholderFillStyle && tiledImage._hasOpaqueTile === false ) {
var placeholderRect = this.viewportToDrawerRectangle(tiledImage.getBounds(true));
if (sketchScale) {
placeholderRect = placeholderRect.times(sketchScale);
}
if (sketchTranslate) {
placeholderRect = placeholderRect.translate(sketchTranslate);
}
var fillStyle = null;
if ( typeof tiledImage.placeholderFillStyle === "function" ) {
fillStyle = tiledImage.placeholderFillStyle(tiledImage, this.context);
}
else {
fillStyle = tiledImage.placeholderFillStyle;
}
this.drawRectangle(placeholderRect, fillStyle, useSketch);
}
var subPixelRoundingRule = determineSubPixelRoundingRule(tiledImage.subPixelRoundingForTransparency);
var shouldRoundPositionAndSize = false;
if (subPixelRoundingRule === $.SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS) {
shouldRoundPositionAndSize = true;
} else if (subPixelRoundingRule === $.SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST) {
var isAnimating = this.viewer && this.viewer.isAnimating();
shouldRoundPositionAndSize = !isAnimating;
}
for (var i = lastDrawn.length - 1; i >= 0; i--) {
tile = lastDrawn[ i ];
this.drawTile( tile, tiledImage._drawingHandler, useSketch, sketchScale,
sketchTranslate, shouldRoundPositionAndSize, tiledImage.source );
tile.beingDrawn = true;
if( this.viewer ){
/**
* <em>- Needs documentation -</em>
*
* @event tile-drawn
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn.
* @property {OpenSeadragon.Tile} tile
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.viewer.raiseEvent( 'tile-drawn', {
tiledImage: tiledImage,
tile: tile
});
}
}
if ( usedClip ) {
this.restoreContext( useSketch );
}
if (!sketchScale) {
if (tiledImage.getRotation(true) % 360 !== 0) {
this._restoreRotationChanges(useSketch);
}
if (this.viewport.getRotation(true) % 360 !== 0) {
this._restoreRotationChanges(useSketch);
}
}
if (useSketch) {
if (sketchScale) {
if (this.viewport.getRotation(true) % 360 !== 0) {
this._offsetForRotation({
degrees: this.viewport.getRotation(true),
useSketch: false
});
}
if (tiledImage.getRotation(true) % 360 !== 0) {
this._offsetForRotation({
degrees: tiledImage.getRotation(true),
point: this.viewport.pixelFromPointNoRotate(
tiledImage._getRotationPoint(true), true),
useSketch: false
});
}
}
this.blendSketch({
opacity: tiledImage.opacity,
scale: sketchScale,
translate: sketchTranslate,
compositeOperation: tiledImage.compositeOperation,
bounds: bounds
});
if (sketchScale) {
if (tiledImage.getRotation(true) % 360 !== 0) {
this._restoreRotationChanges(false);
}
if (this.viewport.getRotation(true) % 360 !== 0) {
this._restoreRotationChanges(false);
}
}
}
if (!sketchScale) {
if (this.viewport.getRotation(true) % 360 === 0 &&
tiledImage.getRotation(true) % 360 === 0) {
if(this.viewer.viewport.getFlip()) {
this._flip();
}
}
}
this._drawDebugInfo( tiledImage, lastDrawn );
},
/**
* @private
* @inner
* Draws special debug information for a TiledImage if in debug mode.
* @param {OpenSeadragon.Tile[]} lastDrawn - An unordered list of Tiles drawn last frame.
*/
_drawDebugInfo: function( tiledImage, lastDrawn ) {
if( tiledImage.debugMode ) {
for ( var i = lastDrawn.length - 1; i >= 0; i-- ) {
var tile = lastDrawn[ i ];
try {
this.drawDebugInfo(tile, lastDrawn.length, i, tiledImage);
} catch(e) {
$.console.error(e);
}
}
}
},
/* Methods from Tile */
/**
* This function converts the given point from to the drawer coordinate by
* multiplying it with the pixel density.
* This function does not take rotation into account, thus assuming provided
* point is at 0 degree.
* @param {OpenSeadragon.Point} point - the pixel point to convert
* @returns {OpenSeadragon.Point} Point in drawer coordinate system.
*/
viewportCoordToDrawerCoord: function(point) {
var vpPoint = this.viewport.pixelFromPointNoRotate(point, true);
return new $.Point(
vpPoint.x * $.pixelDensityRatio,
vpPoint.y * $.pixelDensityRatio
);
},
/**
* This function will create multiple polygon paths on the drawing context by provided polygons,
* then clip the context to the paths.
* @param {OpenSeadragon.Point[][]} polygons - an array of polygons. A polygon is an array of OpenSeadragon.Point
* @param {Boolean} useSketch - Whether to use the sketch canvas or not.
*/
clipWithPolygons: function (polygons, useSketch) {
if (!this.useCanvas) {
return;
}
var context = this._getContext(useSketch);
context.beginPath();
polygons.forEach(function (polygon) {
polygon.forEach(function (coord, i) {
context[i === 0 ? 'moveTo' : 'lineTo'](coord.x, coord.y);
});
});
context.clip();
},
/**
* Scale from OpenSeadragon viewer rectangle to drawer rectangle
* (ignoring rotation)
@ -276,12 +667,179 @@ $.Drawer.prototype = {
if (this.useCanvas) {
var context = this._getContext(useSketch);
scale = scale || 1;
tile.drawCanvas(context, drawingHandler, scale, translate, shouldRoundPositionAndSize, source);
this.drawTileToCanvas(tile, context, drawingHandler, scale, translate, shouldRoundPositionAndSize, source);
} else {
tile.drawHTML( this.canvas );
tile.drawTileToHTML( tile, this.canvas );
}
},
/**
* Renders the tile in a canvas-based context.
* @function
* @param {OpenSeadragon.Tile} tile - the tile to draw to the canvas
* @param {Canvas} context
* @param {Function} drawingHandler - Method for firing the drawing event.
* drawingHandler({context, tile, rendered})
* where <code>rendered</code> is the context with the pre-drawn image.
* @param {Number} [scale=1] - Apply a scale to position and size
* @param {OpenSeadragon.Point} [translate] - A translation vector
* @param {Boolean} [shouldRoundPositionAndSize] - Tells whether to round
* position and size of tiles supporting alpha channel in non-transparency
* context.
* @param {OpenSeadragon.TileSource} source - The source specification of the tile.
*/
drawTileToCanvas: function( tile, context, drawingHandler, scale, translate, shouldRoundPositionAndSize, source) {
var position = tile.position.times($.pixelDensityRatio),
size = tile.size.times($.pixelDensityRatio),
rendered;
if (!tile.context2D && !tile.cacheImageRecord) {
$.console.warn(
'[Drawer.drawTileToCanvas] attempting to draw tile %s when it\'s not cached',
tile.toString());
return;
}
rendered = tile.getCanvasContext();
if ( !tile.loaded || !rendered ){
$.console.warn(
"Attempting to draw tile %s when it's not yet loaded.",
tile.toString()
);
return;
}
context.save();
context.globalAlpha = this.opacity;
if (typeof scale === 'number' && scale !== 1) {
// draw tile at a different scale
position = position.times(scale);
size = size.times(scale);
}
if (translate instanceof $.Point) {
// shift tile position slightly
position = position.plus(translate);
}
//if we are supposed to be rendering fully opaque rectangle,
//ie its done fading or fading is turned off, and if we are drawing
//an image with an alpha channel, then the only way
//to avoid seeing the tile underneath is to clear the rectangle
if (context.globalAlpha === 1 && tile.hasTransparency) {
if (shouldRoundPositionAndSize) {
// Round to the nearest whole pixel so we don't get seams from overlap.
position.x = Math.round(position.x);
position.y = Math.round(position.y);
size.x = Math.round(size.x);
size.y = Math.round(size.y);
}
//clearing only the inside of the rectangle occupied
//by the png prevents edge flikering
context.clearRect(
position.x,
position.y,
size.x,
size.y
);
}
// This gives the application a chance to make image manipulation
// changes as we are rendering the image
drawingHandler({context: context, tile: tile, rendered: rendered});
var sourceWidth, sourceHeight;
if (tile.sourceBounds) {
sourceWidth = Math.min(tile.sourceBounds.width, rendered.canvas.width);
sourceHeight = Math.min(tile.sourceBounds.height, rendered.canvas.height);
} else {
sourceWidth = rendered.canvas.width;
sourceHeight = rendered.canvas.height;
}
context.translate(position.x + size.x / 2, 0);
if (tile.flipped) {
context.scale(-1, 1);
}
context.drawImage(
rendered.canvas,
0,
0,
sourceWidth,
sourceHeight,
-size.x / 2,
position.y,
size.x,
size.y
);
context.restore();
},
/**
* Renders the tile in an html container.
* @function
* @param {OpenSeadragon.Tile} tile
* @param {Element} container
*/
drawTileToHTML: function( tile, container ) {
if (!tile.cacheImageRecord) {
$.console.warn(
'[Drawer.drawTileToHTML] attempting to draw tile %s when it\'s not cached',
tile.toString());
return;
}
if ( !tile.loaded ) {
$.console.warn(
"Attempting to draw tile %s when it's not yet loaded.",
tile.toString()
);
return;
}
//EXPERIMENTAL - trying to figure out how to scale the container
// content during animation of the container size.
if ( !tile.element ) {
var image = tile.getImage();
if (!image) {
return;
}
tile.element = $.makeNeutralElement( "div" );
tile.imgElement = image.cloneNode();
tile.imgElement.style.msInterpolationMode = "nearest-neighbor";
tile.imgElement.style.width = "100%";
tile.imgElement.style.height = "100%";
tile.style = tile.element.style;
tile.style.position = "absolute";
}
if ( tile.element.parentNode !== container ) {
container.appendChild( tile.element );
}
if ( tile.imgElement.parentNode !== tile.element ) {
tile.element.appendChild( tile.imgElement );
}
tile.style.top = tile.position.y + "px";
tile.style.left = tile.position.x + "px";
tile.style.height = tile.size.y + "px";
tile.style.width = tile.size.x + "px";
if (tile.flipped) {
tile.style.transform = "scaleX(-1)";
}
$.setElementOpacity( tile.element, tile.opacity );
},
_getContext: function( useSketch ) {
var context = this.context;
if ( useSketch ) {
@ -766,6 +1324,75 @@ $.Drawer.prototype = {
}
return maxOpacity;
},
};
});
/**
* @private
* @inner
* Defines the value for subpixel rounding to fallback to in case of missing or
* invalid value.
*/
var DEFAULT_SUBPIXEL_ROUNDING_RULE = $.SUBPIXEL_ROUNDING_OCCURRENCES.NEVER;
/**
* @private
* @inner
* Checks whether the input value is an invalid subpixel rounding enum value.
*
* @param {SUBPIXEL_ROUNDING_OCCURRENCES} value - The subpixel rounding enum value to check.
* @returns {Boolean} Returns true if the input value is none of the expected
* {@link SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS}, {@link SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST} or {@link SUBPIXEL_ROUNDING_OCCURRENCES.NEVER} value.
*/
function isSubPixelRoundingRuleUnknown(value) {
return value !== $.SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS &&
value !== $.SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST &&
value !== $.SUBPIXEL_ROUNDING_OCCURRENCES.NEVER;
}
/**
* @private
* @inner
* Ensures the returned value is always a valid subpixel rounding enum value,
* defaulting to {@link SUBPIXEL_ROUNDING_OCCURRENCES.NEVER} if input is missing or invalid.
*
* @param {SUBPIXEL_ROUNDING_OCCURRENCES} value - The subpixel rounding enum value to normalize.
* @returns {SUBPIXEL_ROUNDING_OCCURRENCES} Returns a valid subpixel rounding enum value.
*/
function normalizeSubPixelRoundingRule(value) {
if (isSubPixelRoundingRuleUnknown(value)) {
return DEFAULT_SUBPIXEL_ROUNDING_RULE;
}
return value;
}
/**
* @private
* @inner
* Ensures the returned value is always a valid subpixel rounding enum value,
* defaulting to 'NEVER' if input is missing or invalid.
*
* @param {Object} subPixelRoundingRules - A subpixel rounding enum values dictionary [{@link BROWSERS}] --> {@link SUBPIXEL_ROUNDING_OCCURRENCES}.
* @returns {SUBPIXEL_ROUNDING_OCCURRENCES} Returns the determined subpixel rounding enum value for the
* current browser.
*/
function determineSubPixelRoundingRule(subPixelRoundingRules) {
if (typeof subPixelRoundingRules === 'number') {
return normalizeSubPixelRoundingRule(subPixelRoundingRules);
}
if (!subPixelRoundingRules || !$.Browser) {
return DEFAULT_SUBPIXEL_ROUNDING_RULE;
}
var subPixelRoundingRule = subPixelRoundingRules[$.Browser.vendor];
if (isSubPixelRoundingRuleUnknown(subPixelRoundingRule)) {
subPixelRoundingRule = subPixelRoundingRules['*'];
}
return normalizeSubPixelRoundingRule(subPixelRoundingRule);
}
}( OpenSeadragon ));

344
src/drawerbase.js Normal file
View File

@ -0,0 +1,344 @@
/*
* OpenSeadragon - DrawerBase
*
* Copyright (C) 2009 CodePlex Foundation
* Copyright (C) 2010-2023 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of CodePlex Foundation nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function( $ ){
/**
* @class DrawerBase
* @memberof OpenSeadragon
* @classdesc Base class for Drawers that handle rendering of tiles for an {@link OpenSeadragon.Viewer}.
* @param {Object} options - Options for this Drawer.
* @param {OpenSeadragon.Viewer} options.viewer - The Viewer that owns this Drawer.
* @param {OpenSeadragon.Viewport} options.viewport - Reference to Viewer viewport.
* @param {Element} options.element - Parent element.
*/
$.DrawerBase = function( options ) {
$.console.assert( options.viewer, "[Drawer] options.viewer is required" );
//backward compatibility for positional args while preferring more
//idiomatic javascript options object as the only argument
var args = arguments;
if( !$.isPlainObject( options ) ){
options = {
source: args[ 0 ], // Reference to Viewer tile source.
viewport: args[ 1 ], // Reference to Viewer viewport.
element: args[ 2 ] // Parent element.
};
}
$.console.assert( options.viewport, "[Drawer] options.viewport is required" );
$.console.assert( options.element, "[Drawer] options.element is required" );
if ( options.source ) {
$.console.error( "[Drawer] options.source is no longer accepted; use TiledImage instead" );
}
this.viewer = options.viewer;
this.viewport = options.viewport;
this.debugGridColor = typeof options.debugGridColor === 'string' ? [options.debugGridColor] : options.debugGridColor || $.DEFAULT_SETTINGS.debugGridColor;
if (options.opacity) {
$.console.error( "[Drawer] options.opacity is no longer accepted; set the opacity on the TiledImage instead" );
}
this.useCanvas = $.supportsCanvas && ( this.viewer ? this.viewer.useCanvas : true );
/**
* The parent element of this Drawer instance, passed in when the Drawer was created.
* The parent of {@link OpenSeadragon.DrawerBase#canvas}.
* @member {Element} container
* @memberof OpenSeadragon.DrawerBase#
*/
this.container = $.getElement( options.element );
/**
* A &lt;canvas&gt; element if the browser supports them, otherwise a &lt;div&gt; element.
* Child element of {@link OpenSeadragon.DrawerBase#container}.
* @member {Element} canvas
* @memberof OpenSeadragon.DrawerBase#
*/
this.canvas = $.makeNeutralElement( this.useCanvas ? "canvas" : "div" );
/**
* @member {Element} element
* @memberof OpenSeadragon.DrawerBase#
* @deprecated Alias for {@link OpenSeadragon.DrawerBase#container}.
*/
this.element = this.container;
// We force our container to ltr because our drawing math doesn't work in rtl.
// This issue only affects our canvas renderer, but we do it always for consistency.
// Note that this means overlays you want to be rtl need to be explicitly set to rtl.
this.container.dir = 'ltr';
if (this.useCanvas) {
var viewportSize = this._calculateCanvasSize();
this.canvas.width = viewportSize.x;
this.canvas.height = viewportSize.y;
}
this.canvas.style.width = "100%";
this.canvas.style.height = "100%";
this.canvas.style.position = "absolute";
$.setElementOpacity( this.canvas, this.opacity, true );
// Allow pointer events to pass through the canvas element so implicit
// pointer capture works on touch devices
$.setElementPointerEventsNone( this.canvas );
$.setElementTouchActionNone( this.canvas );
// explicit left-align
this.container.style.textAlign = "left";
this.container.appendChild( this.canvas );
// Image smoothing for canvas rendering (only if canvas is used).
// Canvas default is "true", so this will only be changed if user specified "false".
this._imageSmoothingEnabled = true;
this._checkForAPIOverrides();
};
/** @lends OpenSeadragon.DrawerBaseBase.prototype */
$.DrawerBase.prototype = {
// Drawer implementaions must define the next four methods. These are called
// by core OSD, and forcing overrides (even for nullop methods) makes the
// behavior of the implementations explicitly clear in the code.
// Whether these have been overridden by child classes is checked in the
// constructor (via _checkForAPIOverrides). It could make sense to consolidate
// these a bit (e.g. by making `draw` take an array of `TiledImage`s and
// clearing the view as needed, rather than the existing pattern of
// `drawer.clear(); world.draw()` in the calling code), but they have been
// left as-is to maintain backwards compatibility.
/**
* @param tiledImage the TiledImage that is ready to be drawn
*/
draw: function(tiledImage) {
$.console.error('Drawer.draw must be implemented by child class');
},
/**
* @returns {Boolean} True if rotation is supported.
*/
canRotate: function() {
$.console.error('Drawer.canRotate must be implemented by child class');
},
/**
* Destroy the drawer (unload current loaded tiles)
*/
destroy: function() {
$.console.error('Drawer.destroy must be implemented by child class');
},
/**
* Clears the Drawer so it's ready to draw another frame.
*/
clear: function() {
$.console.error('Drawer.clear must be implemented by child class');
},
/**
* Turns image smoothing on or off for this viewer. Note: Ignored in some (especially older) browsers that do not support this property.
*
* @function
* @param {Boolean} [imageSmoothingEnabled] - Whether or not the image is
* drawn smoothly on the canvas; see imageSmoothingEnabled in
* {@link OpenSeadragon.Options} for more explanation.
*/
setImageSmoothingEnabled: function(imageSmoothingEnabled){
$.console.error('Drawer.setImageSmoothingEnabled must be implemented by child class');
},
/**
* Ensures that child classes have provided implementations for API methods
* draw, canRotate, destroy, and clear. Throws an exception if the original
* placeholder methods are still in place.
*/
_checkForAPIOverrides: function(){
if(this.draw === $.DrawerBase.prototype.draw){
throw("[drawer].draw must be implemented by child class");
}
if(this.canRotate === $.DrawerBase.prototype.canRotate){
throw("[drawer].canRotate must be implemented by child class");
}
if(this.destroy === $.DrawerBase.prototype.destroy){
throw("[drawer].destroy must be implemented by child class");
}
if(this.clear === $.DrawerBase.prototype.clear){
throw("[drawer].clear must be implemented by child class");
}
if(this.setImageSmoothingEnabled === $.DrawerBase.prototype.setImageSmoothingEnabled){
throw("[drawer].setImageSmoothingEnabled must be implemented by child class");
}
},
/**
* Scale from OpenSeadragon viewer rectangle to drawer rectangle
* (ignoring rotation)
* @param {OpenSeadragon.Rect} rectangle - The rectangle in viewport coordinate system.
* @returns {OpenSeadragon.Rect} Rectangle in drawer coordinate system.
*/
viewportToDrawerRectangle: function(rectangle) {
var topLeft = this.viewport.pixelFromPointNoRotate(rectangle.getTopLeft(), true);
var size = this.viewport.deltaPixelsFromPointsNoRotate(rectangle.getSize(), true);
return new $.Rect(
topLeft.x * $.pixelDensityRatio,
topLeft.y * $.pixelDensityRatio,
size.x * $.pixelDensityRatio,
size.y * $.pixelDensityRatio
);
},
/**
* This function converts the given point from to the drawer coordinate by
* multiplying it with the pixel density.
* This function does not take rotation into account, thus assuming provided
* point is at 0 degree.
* @param {OpenSeadragon.Point} point - the pixel point to convert
* @returns {OpenSeadragon.Point} Point in drawer coordinate system.
*/
viewportCoordToDrawerCoord: function(point) {
var vpPoint = this.viewport.pixelFromPointNoRotate(point, true);
return new $.Point(
vpPoint.x * $.pixelDensityRatio,
vpPoint.y * $.pixelDensityRatio
);
},
// private
_calculateCanvasSize: function() {
var pixelDensityRatio = $.pixelDensityRatio;
var viewportSize = this.viewport.getContainerSize();
return {
// canvas width and height are integers
x: Math.round(viewportSize.x * pixelDensityRatio),
y: Math.round(viewportSize.y * pixelDensityRatio)
};
},
/* Deprecated Functions */
// deprecated
addOverlay: function( element, location, placement, onDraw ) {
$.console.error("drawer.addOverlay is deprecated. Use viewer.addOverlay instead.");
this.viewer.addOverlay( element, location, placement, onDraw );
return this;
},
// deprecated
updateOverlay: function( element, location, placement ) {
$.console.error("drawer.updateOverlay is deprecated. Use viewer.updateOverlay instead.");
this.viewer.updateOverlay( element, location, placement );
return this;
},
// deprecated
removeOverlay: function( element ) {
$.console.error("drawer.removeOverlay is deprecated. Use viewer.removeOverlay instead.");
this.viewer.removeOverlay( element );
return this;
},
// deprecated
clearOverlays: function() {
$.console.error("drawer.clearOverlays is deprecated. Use viewer.clearOverlays instead.");
this.viewer.clearOverlays();
return this;
},
// deprecated
needsUpdate: function() {
$.console.error( "[Drawer.needsUpdate] this function is deprecated. Use World.needsDraw instead." );
return this.viewer.world.needsDraw();
},
// deprecated
numTilesLoaded: function() {
$.console.error( "[Drawer.numTilesLoaded] this function is deprecated. Use TileCache.numTilesLoaded instead." );
return this.viewer.tileCache.numTilesLoaded();
},
// deprecated
reset: function() {
$.console.error( "[Drawer.reset] this function is deprecated. Use World.resetItems instead." );
this.viewer.world.resetItems();
return this;
},
// deprecated
update: function() {
$.console.error( "[Drawer.update] this function is deprecated. Use Drawer.clear and World.draw instead." );
this.clear();
this.viewer.world.draw();
return this;
},
// deprecated
setOpacity: function( opacity ) {
$.console.error("drawer.setOpacity is deprecated. Use tiledImage.setOpacity instead.");
var world = this.viewer.world;
for (var i = 0; i < world.getItemCount(); i++) {
world.getItemAt( i ).setOpacity( opacity );
}
return this;
},
// deprecated
getOpacity: function() {
$.console.error("drawer.getOpacity is deprecated. Use tiledImage.getOpacity instead.");
var world = this.viewer.world;
var maxOpacity = 0;
for (var i = 0; i < world.getItemCount(); i++) {
var opacity = world.getItemAt( i ).getOpacity();
if ( opacity > maxOpacity ) {
maxOpacity = opacity;
}
}
return maxOpacity;
},
};
Object.defineProperty($.DrawerBase.prototype, "isOpenSeadragonDrawer", {
get: function get() {
return true;
}
});
}( OpenSeadragon ));

View File

@ -274,64 +274,6 @@
return !!this.context2D || this.getUrl().match('.png');
},
/**
* Renders the tile in an html container.
* @function
* @param {Element} container
*/
drawHTML: function( container ) {
if (!this.cacheImageRecord) {
$.console.warn(
'[Tile.drawHTML] attempting to draw tile %s when it\'s not cached',
this.toString());
return;
}
if ( !this.loaded ) {
$.console.warn(
"Attempting to draw tile %s when it's not yet loaded.",
this.toString()
);
return;
}
//EXPERIMENTAL - trying to figure out how to scale the container
// content during animation of the container size.
if ( !this.element ) {
var image = this.getImage();
if (!image) {
return;
}
this.element = $.makeNeutralElement( "div" );
this.imgElement = image.cloneNode();
this.imgElement.style.msInterpolationMode = "nearest-neighbor";
this.imgElement.style.width = "100%";
this.imgElement.style.height = "100%";
this.style = this.element.style;
this.style.position = "absolute";
}
if ( this.element.parentNode !== container ) {
container.appendChild( this.element );
}
if ( this.imgElement.parentNode !== this.element ) {
this.element.appendChild( this.imgElement );
}
this.style.top = this.position.y + "px";
this.style.left = this.position.x + "px";
this.style.height = this.size.y + "px";
this.style.width = this.size.x + "px";
if (this.flipped) {
this.style.transform = "scaleX(-1)";
}
$.setElementOpacity( this.element, this.opacity );
},
/**
* The Image object for this tile.
* @member {Object} image
@ -385,113 +327,6 @@
return this.context2D || this.cacheImageRecord.getRenderedContext();
},
/**
* Renders the tile in a canvas-based context.
* @function
* @param {Canvas} context
* @param {Function} drawingHandler - Method for firing the drawing event.
* drawingHandler({context, tile, rendered})
* where <code>rendered</code> is the context with the pre-drawn image.
* @param {Number} [scale=1] - Apply a scale to position and size
* @param {OpenSeadragon.Point} [translate] - A translation vector
* @param {Boolean} [shouldRoundPositionAndSize] - Tells whether to round
* position and size of tiles supporting alpha channel in non-transparency
* context.
* @param {OpenSeadragon.TileSource} source - The source specification of the tile.
*/
drawCanvas: function( context, drawingHandler, scale, translate, shouldRoundPositionAndSize, source) {
var position = this.position.times($.pixelDensityRatio),
size = this.size.times($.pixelDensityRatio),
rendered;
if (!this.context2D && !this.cacheImageRecord) {
$.console.warn(
'[Tile.drawCanvas] attempting to draw tile %s when it\'s not cached',
this.toString());
return;
}
rendered = this.getCanvasContext();
if ( !this.loaded || !rendered ){
$.console.warn(
"Attempting to draw tile %s when it's not yet loaded.",
this.toString()
);
return;
}
context.save();
context.globalAlpha = this.opacity;
if (typeof scale === 'number' && scale !== 1) {
// draw tile at a different scale
position = position.times(scale);
size = size.times(scale);
}
if (translate instanceof $.Point) {
// shift tile position slightly
position = position.plus(translate);
}
//if we are supposed to be rendering fully opaque rectangle,
//ie its done fading or fading is turned off, and if we are drawing
//an image with an alpha channel, then the only way
//to avoid seeing the tile underneath is to clear the rectangle
if (context.globalAlpha === 1 && this.hasTransparency) {
if (shouldRoundPositionAndSize) {
// Round to the nearest whole pixel so we don't get seams from overlap.
position.x = Math.round(position.x);
position.y = Math.round(position.y);
size.x = Math.round(size.x);
size.y = Math.round(size.y);
}
//clearing only the inside of the rectangle occupied
//by the png prevents edge flikering
context.clearRect(
position.x,
position.y,
size.x,
size.y
);
}
// This gives the application a chance to make image manipulation
// changes as we are rendering the image
drawingHandler({context: context, tile: this, rendered: rendered});
var sourceWidth, sourceHeight;
if (this.sourceBounds) {
sourceWidth = Math.min(this.sourceBounds.width, rendered.canvas.width);
sourceHeight = Math.min(this.sourceBounds.height, rendered.canvas.height);
} else {
sourceWidth = rendered.canvas.width;
sourceHeight = rendered.canvas.height;
}
context.translate(position.x + size.x / 2, 0);
if (this.flipped) {
context.scale(-1, 1);
}
context.drawImage(
rendered.canvas,
0,
0,
sourceWidth,
sourceHeight,
-size.x / 2,
position.y,
size.x,
size.y
);
context.restore();
},
/**
* Get the ratio between current and original size.
* @function

View File

@ -163,6 +163,7 @@ $.TiledImage = function( options ) {
_needsDraw: true, // Does the tiledImage need to update the viewport again?
_hasOpaqueTile: false, // Do we have even one fully opaque tile?
_tilesLoading: 0, // The number of pending tile requests.
_tilesToDraw: [], // info about the tiles currently in the viewport
//configurable settings
springStiffness: $.DEFAULT_SETTINGS.springStiffness,
animationTime: $.DEFAULT_SETTINGS.animationTime,
@ -305,6 +306,8 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
var scaleUpdated = this._scaleSpring.update();
var degreesUpdated = this._degreesSpring.update();
this._updateTilesForViewport();
if (xUpdated || yUpdated || scaleUpdated || degreesUpdated) {
this._updateForScale();
this._raiseBoundsChange();
@ -315,21 +318,6 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
return false;
},
/**
* Draws the TiledImage to its Drawer.
*/
draw: function() {
if (this.opacity !== 0 || this._preload) {
this._midDraw = true;
this._updateViewport();
this._midDraw = false;
}
// Images with opacity 0 should not need to be drawn in future. this._needsDraw = false is set in this._updateViewport() for other images.
else {
this._needsDraw = false;
}
},
/**
* Destroy the TiledImage (unload current loaded tiles).
*/
@ -767,7 +755,6 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
* ]
*/
setCroppingPolygons: function( polygons ) {
var isXYObject = function(obj) {
return obj instanceof $.Point || (typeof obj.x === 'number' && typeof obj.y === 'number');
};
@ -793,10 +780,11 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
this._croppingPolygons = polygons.map(function(polygon){
return objectToSimpleXYObject(polygon);
});
this._needsDraw = true;
} catch (e) {
$.console.error('[TiledImage.setCroppingPolygons] Cropping polygon format not supported');
$.console.error(e);
this._croppingPolygons = null;
this.resetCroppingPolygons();
}
},
@ -806,6 +794,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
*/
resetCroppingPolygons: function() {
this._croppingPolygons = null;
this._needsDraw = true;
},
/**
@ -1007,6 +996,24 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
this._raiseBoundsChange();
},
/**
* Get the region of this tiled image that falls within the viewport.
* @returns OpenSeadragon.Rect
*/
getDrawArea: function(){
var drawArea = this._viewportToTiledImageRectangle(
this.viewport.getBoundsWithMargins(true));
if (!this.wrapHorizontal && !this.wrapVertical) {
var tiledImageBounds = this._viewportToTiledImageRectangle(
this.getClippedBounds(true));
drawArea = drawArea.intersection(tiledImageBounds);
}
return drawArea;
},
/**
* Get the point around which this tiled image is rotated
* @private
@ -1029,6 +1036,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
* @fires OpenSeadragon.TiledImage.event:composite-operation-change
*/
setCompositeOperation: function(compositeOperation) {
var _this = this;
if (compositeOperation === this.compositeOperation) {
return;
}
@ -1048,6 +1056,21 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
this.raiseEvent('composite-operation-change', {
compositeOperation: this.compositeOperation
});
/**
* Raised when a TiledImage's opacity is changed.
* @event composite-operation-change
* @memberOf OpenSeadragon.TiledImage
* @type {object}
* @property {String} compositeOperation - The new compositeOperation value.
* @property {OpenSeadragon.Viewer} eventSource - A reference to the
* Viewer which raised the event.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.viewer.raiseEvent('composite-operation-change', {
compositeOperation: _this.compositeOperation,
tiledImage: _this
});
},
/**
@ -1215,50 +1238,34 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
};
},
/**
* @private
* @inner
* Pretty much every other line in this needs to be documented so it's clear
* how each piece of this routine contributes to the drawing process. That's
* why there are so many TODO's inside this function.
*/
_updateViewport: function() {
this._needsDraw = false;
this._tilesLoading = 0;
this.loadingCoverage = {};
// Reset tile's internal drawn state
while (this.lastDrawn.length > 0) {
var tile = this.lastDrawn.pop();
tile.beingDrawn = false;
}
var viewport = this.viewport;
var drawArea = this._viewportToTiledImageRectangle(
viewport.getBoundsWithMargins(true));
if (!this.wrapHorizontal && !this.wrapVertical) {
var tiledImageBounds = this._viewportToTiledImageRectangle(
this.getClippedBounds(true));
drawArea = drawArea.intersection(tiledImageBounds);
if (drawArea === null) {
return;
}
}
getTileInfoForDrawing: function(){
return this._tilesToDraw;
},
_updateTilesForViewport: function(){
var levelsInterval = this._getLevelsInterval();
var lowestLevel = levelsInterval.lowestLevel;
var highestLevel = levelsInterval.highestLevel;
var bestTile = null;
var haveDrawn = false;
var drawArea = this.getDrawArea();
var currentTime = $.now();
this._tilesToDraw = [];
this._tilesLoading = 0;
this.loadingCoverage = {};
if(!drawArea){
this._needsDraw = false;
return;
}
// Update any level that will be drawn
for (var level = highestLevel; level >= lowestLevel; level--) {
var drawLevel = false;
//Avoid calculations for draw if we have already drawn this
var currentRenderPixelRatio = viewport.deltaPixelsFromPointsNoRotate(
var currentRenderPixelRatio = this.viewport.deltaPixelsFromPointsNoRotate(
this.source.getPixelRatio(level),
true
).x * this._scaleSpring.current.value;
@ -1272,12 +1279,12 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
}
//Perform calculations for draw if we haven't drawn this
var targetRenderPixelRatio = viewport.deltaPixelsFromPointsNoRotate(
var targetRenderPixelRatio = this.viewport.deltaPixelsFromPointsNoRotate(
this.source.getPixelRatio(level),
false
).x * this._scaleSpring.current.value;
var targetZeroRatio = viewport.deltaPixelsFromPointsNoRotate(
var targetZeroRatio = this.viewport.deltaPixelsFromPointsNoRotate(
this.source.getPixelRatio(
Math.max(
this.source.getClosestLevel(),
@ -1294,7 +1301,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
);
// Update the level and keep track of 'best' tile to load
bestTile = this._updateLevel(
var result = this._updateLevel(
haveDrawn,
drawLevel,
level,
@ -1305,6 +1312,21 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
bestTile
);
bestTile = result.best;
var tiles = result.tiles;
var makeTileInfoObject = (function(level, levelOpacity, currentTime){
return function(tile){
return {
tile: tile,
level: level,
levelOpacity: levelOpacity,
currentTime: currentTime
};
};
})(level, levelOpacity, currentTime);
this._tilesToDraw = this._tilesToDraw.concat(tiles.map(makeTileInfoObject));
// Stop the loop if lower-res tiles would all be covered by
// already drawn tiles
if (this._providesCoverage(this.coverage, level)) {
@ -1312,11 +1334,6 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
}
}
// Perform the actual drawing
this._drawTiles(this.lastDrawn);
// Load the new 'best' tile
if (bestTile && !bestTile.context2D) {
this._loadTile(bestTile, currentTime);
@ -1325,47 +1342,9 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
} else {
this._setFullyLoaded(this._tilesLoading === 0);
}
},
// private
_getCornerTiles: function(level, topLeftBound, bottomRightBound) {
var leftX;
var rightX;
if (this.wrapHorizontal) {
leftX = $.positiveModulo(topLeftBound.x, 1);
rightX = $.positiveModulo(bottomRightBound.x, 1);
} else {
leftX = Math.max(0, topLeftBound.x);
rightX = Math.min(1, bottomRightBound.x);
}
var topY;
var bottomY;
var aspectRatio = 1 / this.source.aspectRatio;
if (this.wrapVertical) {
topY = $.positiveModulo(topLeftBound.y, aspectRatio);
bottomY = $.positiveModulo(bottomRightBound.y, aspectRatio);
} else {
topY = Math.max(0, topLeftBound.y);
bottomY = Math.min(aspectRatio, bottomRightBound.y);
}
var topLeftTile = this.source.getTileAtPoint(level, new $.Point(leftX, topY));
var bottomRightTile = this.source.getTileAtPoint(level, new $.Point(rightX, bottomY));
var numTiles = this.source.getNumTiles(level);
if (this.wrapHorizontal) {
topLeftTile.x += numTiles.x * Math.floor(topLeftBound.x);
bottomRightTile.x += numTiles.x * Math.floor(bottomRightBound.x);
}
if (this.wrapVertical) {
topLeftTile.y += numTiles.y * Math.floor(topLeftBound.y / aspectRatio);
bottomRightTile.y += numTiles.y * Math.floor(bottomRightBound.y / aspectRatio);
}
return {
topLeft: topLeftTile,
bottomRight: bottomRightTile,
};
// return bestTile;
},
/**
@ -1378,7 +1357,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
* @param {Number} levelVisibility
* @param {OpenSeadragon.Rect} drawArea
* @param {Number} currentTime
* @param {OpenSeadragon.Tile} best - The current "best" tile to draw.
* @param {Object} result Dictionary {best: OpenSeadragon.Tile - the current "best" tile to draw, tiles: Array(OpenSeadragon.Tile) - the updated tiles}.
*/
_updateLevel: function(haveDrawn, drawLevel, level, levelOpacity,
levelVisibility, drawArea, currentTime, best) {
@ -1443,7 +1422,9 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
bottomRightTile.x = Math.min(bottomRightTile.x, numberOfTiles.x - 1);
}
}
var numTiles = Math.max(0, (bottomRightTile.x - topLeftTile.x) * (bottomRightTile.y - topLeftTile.y));
var tiles = new Array(numTiles);
var tileIndex = 0;
for (var x = topLeftTile.x; x <= bottomRightTile.x; x++) {
for (var y = topLeftTile.y; y <= bottomRightTile.y; y++) {
@ -1460,22 +1441,74 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
continue;
}
best = this._updateTile(
var result = this._updateTile(
drawLevel,
haveDrawn,
flippedX, y,
level,
levelOpacity,
levelVisibility,
viewportCenter,
numberOfTiles,
currentTime,
best
);
best = result.best;
tiles[tileIndex] = result.tile;
tileIndex += 1;
}
}
return best;
return {
best: best,
tiles: tiles
};
},
/**
* @private
* @inner
* @param {OpenSeadragon.Tile} tile
* @param {Boolean} overlap
* @param {OpenSeadragon.Viewport} viewport
* @param {OpenSeadragon.Point} viewportCenter
* @param {Number} levelVisibility
*/
_positionTile: function( tile, overlap, viewport, viewportCenter, levelVisibility ){
var boundsTL = tile.bounds.getTopLeft();
boundsTL.x *= this._scaleSpring.current.value;
boundsTL.y *= this._scaleSpring.current.value;
boundsTL.x += this._xSpring.current.value;
boundsTL.y += this._ySpring.current.value;
var boundsSize = tile.bounds.getSize();
boundsSize.x *= this._scaleSpring.current.value;
boundsSize.y *= this._scaleSpring.current.value;
var positionC = viewport.pixelFromPointNoRotate(boundsTL, true),
positionT = viewport.pixelFromPointNoRotate(boundsTL, false),
sizeC = viewport.deltaPixelsFromPointsNoRotate(boundsSize, true),
sizeT = viewport.deltaPixelsFromPointsNoRotate(boundsSize, false),
tileCenter = positionT.plus( sizeT.divide( 2 ) ),
tileSquaredDistance = viewportCenter.squaredDistanceTo( tileCenter );
if ( !overlap ) {
sizeC = sizeC.plus( new $.Point( 1, 1 ) );
}
if (tile.isRightMost && this.wrapHorizontal) {
sizeC.x += 0.75; // Otherwise Firefox and Safari show seams
}
if (tile.isBottomMost && this.wrapVertical) {
sizeC.y += 0.75; // Otherwise Firefox and Safari show seams
}
tile.position = positionC;
tile.size = sizeC;
tile.squaredDistance = tileSquaredDistance;
tile.visibility = levelVisibility;
},
/**
@ -1487,14 +1520,13 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
* @param {Number} x
* @param {Number} y
* @param {Number} level
* @param {Number} levelOpacity
* @param {Number} levelVisibility
* @param {OpenSeadragon.Point} viewportCenter
* @param {Number} numberOfTiles
* @param {Number} currentTime
* @param {OpenSeadragon.Tile} best - The current "best" tile to draw.
*/
_updateTile: function( haveDrawn, drawLevel, x, y, level, levelOpacity,
_updateTile: function( haveDrawn, drawLevel, x, y, level,
levelVisibility, viewportCenter, numberOfTiles, currentTime, best){
var tile = this._getTile(
@ -1531,7 +1563,9 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
this._setCoverage(this.loadingCoverage, level, x, y, loadingCoverage);
if ( !tile.exists ) {
return best;
return {
best: best
};
}
if ( haveDrawn && !drawTile ) {
@ -1543,7 +1577,9 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
}
if ( !drawTile ) {
return best;
return {
best: best
};
}
this._positionTile(
@ -1565,26 +1601,58 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
}
}
if ( tile.loaded ) {
var needsDraw = this._blendTile(
tile,
x, y,
level,
levelOpacity,
currentTime
);
if ( needsDraw ) {
this._needsDraw = true;
}
} else if ( tile.loading ) {
if ( tile.loading ) {
// the tile is already in the download queue
this._tilesLoading++;
} else if (!loadingCoverage) {
best = this._compareTiles( best, tile );
}
return best;
return {
best: best,
tile: tile
};
},
// private
_getCornerTiles: function(level, topLeftBound, bottomRightBound) {
var leftX;
var rightX;
if (this.wrapHorizontal) {
leftX = $.positiveModulo(topLeftBound.x, 1);
rightX = $.positiveModulo(bottomRightBound.x, 1);
} else {
leftX = Math.max(0, topLeftBound.x);
rightX = Math.min(1, bottomRightBound.x);
}
var topY;
var bottomY;
var aspectRatio = 1 / this.source.aspectRatio;
if (this.wrapVertical) {
topY = $.positiveModulo(topLeftBound.y, aspectRatio);
bottomY = $.positiveModulo(bottomRightBound.y, aspectRatio);
} else {
topY = Math.max(0, topLeftBound.y);
bottomY = Math.min(aspectRatio, bottomRightBound.y);
}
var topLeftTile = this.source.getTileAtPoint(level, new $.Point(leftX, topY));
var bottomRightTile = this.source.getTileAtPoint(level, new $.Point(rightX, bottomY));
var numTiles = this.source.getNumTiles(level);
if (this.wrapHorizontal) {
topLeftTile.x += numTiles.x * Math.floor(topLeftBound.x);
bottomRightTile.x += numTiles.x * Math.floor(bottomRightBound.x);
}
if (this.wrapVertical) {
topLeftTile.y += numTiles.y * Math.floor(topLeftBound.y / aspectRatio);
bottomRightTile.y += numTiles.y * Math.floor(bottomRightBound.y / aspectRatio);
}
return {
topLeft: topLeftTile,
bottomRight: bottomRightTile,
};
},
/**
@ -1876,99 +1944,6 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
fallbackCompletion();
},
/**
* @private
* @inner
* @param {OpenSeadragon.Tile} tile
* @param {Boolean} overlap
* @param {OpenSeadragon.Viewport} viewport
* @param {OpenSeadragon.Point} viewportCenter
* @param {Number} levelVisibility
*/
_positionTile: function( tile, overlap, viewport, viewportCenter, levelVisibility ){
var boundsTL = tile.bounds.getTopLeft();
boundsTL.x *= this._scaleSpring.current.value;
boundsTL.y *= this._scaleSpring.current.value;
boundsTL.x += this._xSpring.current.value;
boundsTL.y += this._ySpring.current.value;
var boundsSize = tile.bounds.getSize();
boundsSize.x *= this._scaleSpring.current.value;
boundsSize.y *= this._scaleSpring.current.value;
var positionC = viewport.pixelFromPointNoRotate(boundsTL, true),
positionT = viewport.pixelFromPointNoRotate(boundsTL, false),
sizeC = viewport.deltaPixelsFromPointsNoRotate(boundsSize, true),
sizeT = viewport.deltaPixelsFromPointsNoRotate(boundsSize, false),
tileCenter = positionT.plus( sizeT.divide( 2 ) ),
tileSquaredDistance = viewportCenter.squaredDistanceTo( tileCenter );
if ( !overlap ) {
sizeC = sizeC.plus( new $.Point( 1, 1 ) );
}
if (tile.isRightMost && this.wrapHorizontal) {
sizeC.x += 0.75; // Otherwise Firefox and Safari show seams
}
if (tile.isBottomMost && this.wrapVertical) {
sizeC.y += 0.75; // Otherwise Firefox and Safari show seams
}
tile.position = positionC;
tile.size = sizeC;
tile.squaredDistance = tileSquaredDistance;
tile.visibility = levelVisibility;
},
/**
* @private
* @inner
* Updates the opacity of a tile according to the time it has been on screen
* to perform a fade-in.
* Updates coverage once a tile is fully opaque.
* Returns whether the fade-in has completed.
*
* @param {OpenSeadragon.Tile} tile
* @param {Number} x
* @param {Number} y
* @param {Number} level
* @param {Number} levelOpacity
* @param {Number} currentTime
* @returns {Boolean}
*/
_blendTile: function( tile, x, y, level, levelOpacity, currentTime ){
var blendTimeMillis = 1000 * this.blendTime,
deltaTime,
opacity;
if ( !tile.blendStart ) {
tile.blendStart = currentTime;
}
deltaTime = currentTime - tile.blendStart;
opacity = blendTimeMillis ? Math.min( 1, deltaTime / ( blendTimeMillis ) ) : 1;
if ( this.alwaysBlend ) {
opacity *= levelOpacity;
}
tile.opacity = opacity;
this.lastDrawn.push( tile );
if ( opacity === 1 ) {
this._setCoverage( this.coverage, level, x, y, true );
this._hasOpaqueTile = true;
} else if ( deltaTime < blendTimeMillis ) {
return true;
}
return false;
},
/**
* @private
@ -1995,274 +1970,6 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
return previousBest;
},
/**
* @private
* @inner
* Draws a TiledImage.
* @param {OpenSeadragon.Tile[]} lastDrawn - An unordered list of Tiles drawn last frame.
*/
_drawTiles: function( lastDrawn ) {
if (this.opacity === 0 || (lastDrawn.length === 0 && !this.placeholderFillStyle)) {
return;
}
var tile = lastDrawn[0];
var useSketch;
if (tile) {
useSketch = this.opacity < 1 ||
(this.compositeOperation && this.compositeOperation !== 'source-over') ||
(!this._isBottomItem() &&
this.source.hasTransparency(tile.context2D, tile.getUrl(), tile.ajaxHeaders, tile.postData));
}
var sketchScale;
var sketchTranslate;
var zoom = this.viewport.getZoom(true);
var imageZoom = this.viewportToImageZoom(zoom);
if (lastDrawn.length > 1 &&
imageZoom > this.smoothTileEdgesMinZoom &&
!this.iOSDevice &&
this.getRotation(true) % 360 === 0 && // TODO: support tile edge smoothing with tiled image rotation (viewport rotation is not a problem).
this._drawer.viewer.viewport.getFlip() === false && // TODO: support tile edge smoothing with viewport flip (tiled image flip is not a problem).
$.supportsCanvas && this.viewer.useCanvas) {
// When zoomed in a lot (>100%) the tile edges are visible.
// So we have to composite them at ~100% and scale them up together.
// Note: Disabled on iOS devices per default as it causes a native crash
useSketch = true;
sketchScale = tile.getScaleForEdgeSmoothing();
sketchTranslate = tile.getTranslationForEdgeSmoothing(sketchScale,
this._drawer.getCanvasSize(false),
this._drawer.getCanvasSize(true));
}
var bounds;
if (useSketch) {
if (!sketchScale) {
// Except when edge smoothing, we only clean the part of the
// sketch canvas we are going to use for performance reasons.
bounds = this.viewport.viewportToViewerElementRectangle(
this.getClippedBounds(true))
.getIntegerBoundingBox();
if(this._drawer.viewer.viewport.getFlip()) {
if (this.viewport.getRotation(true) % 360 !== 0 ||
this.getRotation(true) % 360 !== 0) {
bounds.x = this._drawer.viewer.container.clientWidth - (bounds.x + bounds.width);
}
}
bounds = bounds.times($.pixelDensityRatio);
}
this._drawer._clear(true, bounds);
}
// When scaling, we must rotate only when blending the sketch canvas to
// avoid interpolation
if (!sketchScale) {
if (this.viewport.getRotation(true) % 360 !== 0) {
this._drawer._offsetForRotation({
degrees: this.viewport.getRotation(true),
useSketch: useSketch
});
}
if (this.getRotation(true) % 360 !== 0) {
this._drawer._offsetForRotation({
degrees: this.getRotation(true),
point: this.viewport.pixelFromPointNoRotate(
this._getRotationPoint(true), true),
useSketch: useSketch
});
}
if (this.viewport.getRotation(true) % 360 === 0 &&
this.getRotation(true) % 360 === 0) {
if(this._drawer.viewer.viewport.getFlip()) {
this._drawer._flip();
}
}
}
var usedClip = false;
if ( this._clip ) {
this._drawer.saveContext(useSketch);
var box = this.imageToViewportRectangle(this._clip, true);
box = box.rotate(-this.getRotation(true), this._getRotationPoint(true));
var clipRect = this._drawer.viewportToDrawerRectangle(box);
if (sketchScale) {
clipRect = clipRect.times(sketchScale);
}
if (sketchTranslate) {
clipRect = clipRect.translate(sketchTranslate);
}
this._drawer.setClip(clipRect, useSketch);
usedClip = true;
}
if (this._croppingPolygons) {
var self = this;
this._drawer.saveContext(useSketch);
try {
var polygons = this._croppingPolygons.map(function (polygon) {
return polygon.map(function (coord) {
var point = self
.imageToViewportCoordinates(coord.x, coord.y, true)
.rotate(-self.getRotation(true), self._getRotationPoint(true));
var clipPoint = self._drawer.viewportCoordToDrawerCoord(point);
if (sketchScale) {
clipPoint = clipPoint.times(sketchScale);
}
if (sketchTranslate) {
clipPoint = clipPoint.plus(sketchTranslate);
}
return clipPoint;
});
});
this._drawer.clipWithPolygons(polygons, useSketch);
} catch (e) {
$.console.error(e);
}
usedClip = true;
}
if ( this.placeholderFillStyle && this._hasOpaqueTile === false ) {
var placeholderRect = this._drawer.viewportToDrawerRectangle(this.getBounds(true));
if (sketchScale) {
placeholderRect = placeholderRect.times(sketchScale);
}
if (sketchTranslate) {
placeholderRect = placeholderRect.translate(sketchTranslate);
}
var fillStyle = null;
if ( typeof this.placeholderFillStyle === "function" ) {
fillStyle = this.placeholderFillStyle(this, this._drawer.context);
}
else {
fillStyle = this.placeholderFillStyle;
}
this._drawer.drawRectangle(placeholderRect, fillStyle, useSketch);
}
var subPixelRoundingRule = determineSubPixelRoundingRule(this.subPixelRoundingForTransparency);
var shouldRoundPositionAndSize = false;
if (subPixelRoundingRule === $.SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS) {
shouldRoundPositionAndSize = true;
} else if (subPixelRoundingRule === $.SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST) {
var isAnimating = this.viewer && this.viewer.isAnimating();
shouldRoundPositionAndSize = !isAnimating;
}
for (var i = lastDrawn.length - 1; i >= 0; i--) {
tile = lastDrawn[ i ];
this._drawer.drawTile( tile, this._drawingHandler, useSketch, sketchScale,
sketchTranslate, shouldRoundPositionAndSize, this.source );
tile.beingDrawn = true;
if( this.viewer ){
/**
* <em>- Needs documentation -</em>
*
* @event tile-drawn
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn.
* @property {OpenSeadragon.Tile} tile
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.viewer.raiseEvent( 'tile-drawn', {
tiledImage: this,
tile: tile
});
}
}
if ( usedClip ) {
this._drawer.restoreContext( useSketch );
}
if (!sketchScale) {
if (this.getRotation(true) % 360 !== 0) {
this._drawer._restoreRotationChanges(useSketch);
}
if (this.viewport.getRotation(true) % 360 !== 0) {
this._drawer._restoreRotationChanges(useSketch);
}
}
if (useSketch) {
if (sketchScale) {
if (this.viewport.getRotation(true) % 360 !== 0) {
this._drawer._offsetForRotation({
degrees: this.viewport.getRotation(true),
useSketch: false
});
}
if (this.getRotation(true) % 360 !== 0) {
this._drawer._offsetForRotation({
degrees: this.getRotation(true),
point: this.viewport.pixelFromPointNoRotate(
this._getRotationPoint(true), true),
useSketch: false
});
}
}
this._drawer.blendSketch({
opacity: this.opacity,
scale: sketchScale,
translate: sketchTranslate,
compositeOperation: this.compositeOperation,
bounds: bounds
});
if (sketchScale) {
if (this.getRotation(true) % 360 !== 0) {
this._drawer._restoreRotationChanges(false);
}
if (this.viewport.getRotation(true) % 360 !== 0) {
this._drawer._restoreRotationChanges(false);
}
}
}
if (!sketchScale) {
if (this.viewport.getRotation(true) % 360 === 0 &&
this.getRotation(true) % 360 === 0) {
if(this._drawer.viewer.viewport.getFlip()) {
this._drawer._flip();
}
}
}
this._drawDebugInfo( lastDrawn );
},
/**
* @private
* @inner
* Draws special debug information for a TiledImage if in debug mode.
* @param {OpenSeadragon.Tile[]} lastDrawn - An unordered list of Tiles drawn last frame.
*/
_drawDebugInfo: function( lastDrawn ) {
if( this.debugMode ) {
for ( var i = lastDrawn.length - 1; i >= 0; i-- ) {
var tile = lastDrawn[ i ];
try {
this._drawer.drawDebugInfo(tile, lastDrawn.length, i, this);
} catch(e) {
$.console.error(e);
}
}
}
},
/**
* @private
* @inner
@ -2381,71 +2088,5 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
});
/**
* @private
* @inner
* Defines the value for subpixel rounding to fallback to in case of missing or
* invalid value.
*/
var DEFAULT_SUBPIXEL_ROUNDING_RULE = $.SUBPIXEL_ROUNDING_OCCURRENCES.NEVER;
/**
* @private
* @inner
* Checks whether the input value is an invalid subpixel rounding enum value.
*
* @param {SUBPIXEL_ROUNDING_OCCURRENCES} value - The subpixel rounding enum value to check.
* @returns {Boolean} Returns true if the input value is none of the expected
* {@link SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS}, {@link SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST} or {@link SUBPIXEL_ROUNDING_OCCURRENCES.NEVER} value.
*/
function isSubPixelRoundingRuleUnknown(value) {
return value !== $.SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS &&
value !== $.SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST &&
value !== $.SUBPIXEL_ROUNDING_OCCURRENCES.NEVER;
}
/**
* @private
* @inner
* Ensures the returned value is always a valid subpixel rounding enum value,
* defaulting to {@link SUBPIXEL_ROUNDING_OCCURRENCES.NEVER} if input is missing or invalid.
*
* @param {SUBPIXEL_ROUNDING_OCCURRENCES} value - The subpixel rounding enum value to normalize.
* @returns {SUBPIXEL_ROUNDING_OCCURRENCES} Returns a valid subpixel rounding enum value.
*/
function normalizeSubPixelRoundingRule(value) {
if (isSubPixelRoundingRuleUnknown(value)) {
return DEFAULT_SUBPIXEL_ROUNDING_RULE;
}
return value;
}
/**
* @private
* @inner
* Ensures the returned value is always a valid subpixel rounding enum value,
* defaulting to 'NEVER' if input is missing or invalid.
*
* @param {Object} subPixelRoundingRules - A subpixel rounding enum values dictionary [{@link BROWSERS}] --> {@link SUBPIXEL_ROUNDING_OCCURRENCES}.
* @returns {SUBPIXEL_ROUNDING_OCCURRENCES} Returns the determined subpixel rounding enum value for the
* current browser.
*/
function determineSubPixelRoundingRule(subPixelRoundingRules) {
if (typeof subPixelRoundingRules === 'number') {
return normalizeSubPixelRoundingRule(subPixelRoundingRules);
}
if (!subPixelRoundingRules || !$.Browser) {
return DEFAULT_SUBPIXEL_ROUNDING_RULE;
}
var subPixelRoundingRule = subPixelRoundingRules[$.Browser.vendor];
if (isSubPixelRoundingRuleUnknown(subPixelRoundingRule)) {
subPixelRoundingRule = subPixelRoundingRules['*'];
}
return normalizeSubPixelRoundingRule(subPixelRoundingRule);
}
}( OpenSeadragon ));

View File

@ -368,7 +368,10 @@ $.TileSource.prototype = {
getTileAtPoint: function(level, point) {
var validPoint = point.x >= 0 && point.x <= 1 &&
point.y >= 0 && point.y <= 1 / this.aspectRatio;
$.console.assert(validPoint, "[TileSource.getTileAtPoint] must be called with a valid point.");
// $.console.assert(validPoint, "[TileSource.getTileAtPoint] must be called with a valid point.");
if(!validPoint){
$.console.warn("[TileSource.getTileAtPoint] called with an invalid point.");
}
var widthScaled = this.dimensions.x * this.getLevelScale(level);
var pixelX = point.x * widthScaled;

View File

@ -419,12 +419,29 @@ $.Viewer = function( options ) {
});
// Create the drawer
this.drawer = new $.Drawer({
viewer: this,
viewport: this.viewport,
element: this.canvas,
debugGridColor: this.debugGridColor
});
if(this.customDrawer){
if(this.customDrawer.prototype.isOpenSeadragonDrawer){
var Drawer = this.customDrawer;
this.drawer = new Drawer({
viewer: this,
viewport: this.viewport,
element: this.canvas,
debugGridColor: this.debugGridColor
});
} else {
// $.console.error('Viewer option customDrawer must derive from OpenSeadragon.DrawerBase');
throw('Viewer option customDrawer must derive from OpenSeadragon.DrawerBase');
}
} else {
this.drawer = new $.Drawer({
viewer: this,
viewport: this.viewport,
element: this.canvas,
debugGridColor: this.debugGridColor
});
}
// Overlay container
this.overlaysContainer = $.makeNeutralElement( "div" );

View File

@ -257,7 +257,7 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W
*/
draw: function() {
for ( var i = 0; i < this._items.length; i++ ) {
this._items[i].draw();
this.viewer.drawer.draw(this._items[i]);
}
this._needsDraw = false;

796
test/demo/threejsdrawer.js Normal file
View File

@ -0,0 +1,796 @@
// import 'https://cdnjs.cloudflare.com/ajax/libs/three.js/0.149.0/three.min.js';
import '../lib/three.js';
const THREE = window.THREE;
export class ThreeJSDrawer extends OpenSeadragon.DrawerBase{
constructor(options){
super(options);
let _this = this;
// this.viewer set by parent constructor
// this.canvas set by parent constructor, created and appended to the viewer container element
this._camera = null;
this._currentImages = [];
this._renderer = null;
this._tileMap = {};
this._tiledImageMap = {};
this._uuid = generateUUID(); // to use for reference mapping
this._renderingContinously = false;
this._animationFrame = null;
this._outputCanvas = this.canvas; //output canvas
this._outputContext = this._outputCanvas.getContext('2d');
this._renderingCanvas = document.createElement('canvas');
this._clippingCanvas = document.createElement('canvas');
this._clippingContext = this._clippingCanvas.getContext('2d');
this._renderingCanvas.width = this._clippingCanvas.width = this._outputCanvas.width;
this._renderingCanvas.height = this._clippingCanvas.height = this._outputCanvas.height;
//make the additional canvas elements mirror size changes to the output canvas
this.viewer.addHandler("resize", function(){
if(_this._outputCanvas !== _this.viewer.drawer.canvas){
_this._outputCanvas.style.width = _this.viewer.drawer.canvas.clientWidth + 'px';
_this._outputCanvas.style.height = _this.viewer.drawer.canvas.clientHeight + 'px';
}
let viewportSize = _this._calculateCanvasSize();
if( _this._outputCanvas.width !== viewportSize.x ||
_this._outputCanvas.height !== viewportSize.y ) {
_this._outputCanvas.width = viewportSize.x;
_this._outputCanvas.height = viewportSize.y;
_this._renderer.setViewport(0, 0, _this._outputCanvas.width, _this._outputCanvas.height);
}
_this._renderingCanvas.style.width = _this._outputCanvas.clientWidth+'px';
_this._renderingCanvas.style.height = _this._outputCanvas.clientHeight+'px';
_this._renderingCanvas.width = _this._clippingCanvas.width = _this._outputCanvas.width;
_this._renderingCanvas.height = _this._clippingCanvas.height = _this._outputCanvas.height;
_this.render();
})
this._setupRenderer();
}
renderFrame(){
if(this._animationFrame) {
cancelAnimationFrame(this._animationFrame);
}
this._animationFrame = requestAnimationFrame(()=>this.render());
}
render(){
let numItems = this.viewer.world.getItemCount();
this._outputContext.clearRect(0, 0, this._outputCanvas.width, this._outputCanvas.height);
//iterate over items to draw
for(let i = 0; i < numItems; i++){
let item = this.viewer.world.getItemAt(i);
let scene = this._tiledImageMap[item[this._uuid]];
if(item.wrapHorizontal || item.wrapVertical){
createWrappingGrid(scene, item);
} else {
scene.userData.wrappedCopies.clear();
}
this._renderer.render(scene, this._camera); //renders to this._renderingCanvas
this._outputContext.save();
// set composite operation; ignore for first image drawn
this._outputContext.globalCompositeOperation = i===0 ? null : item.compositeOperation || this.viewer.compositeOperation;
if(item._croppingPolygons || item._clip){
this._renderToClippingCanvas(item);
this._outputContext.drawImage(this._clippingCanvas, 0, 0);
} else {
this._outputContext.drawImage(this._renderingCanvas, 0, 0);
}
this._outputContext.restore();
if(item.debugMode){
this._drawDebugInfo(item)
}
}
this._animationFrame = null;
if(this._renderingContinuously){
this.renderFrame();
}
// console.log(this._renderer.info.memory, this._renderer.info.render.triangles);
}
renderContinuously(continuously){
if(continuously){
this._renderingContinuously = true;
} else {
this._renderingContinuously = false;
}
}
// Public API required by all Drawer implementations
destroy(){
// clear all resources used by the renderer, geometries, textures etc
// to do: remove handlers from viewer
// dispose of any remaining textures/materials
Object.values(this._tileMap).forEach(material=>material.dispose());
//to do: check whether tiled images are all removed (for clean up) before viewer destroy event
// clean up renderer and camera objects
cleanupObject(this._renderer);
cleanupObject(this._camera);
this._renderer = null;
this._camera = null;
}
clear(){
//not needed by this implementation
}
canRotate(){
return true;
}
draw(tiledImage){
// actual drawing is handled by event listeneners
// just mark this tiledImage as having been drawn (possibly unnecessary)
tiledImage._needsDraw = false;
}
setImageSmoothingEnabled(enabled){
this._clippingContext.imageSmoothingEnabled = enabled;
this._outputContext.imageSmoothingEnabled = enabled;
}
// Private methods
_setupRenderer(){
//to do: test support for pages of sequence mode
let viewerBounds = this.viewer.viewport.getBoundsNoRotate();
this._camera = new THREE.OrthographicCamera(
viewerBounds.width / -2,
viewerBounds.width / 2,
viewerBounds.height / 2,
viewerBounds.height / -2,
0,
10000
);
this._camera.position.x = viewerBounds.x + viewerBounds.width/2;
this._camera.position.y = -(viewerBounds.y + viewerBounds.height/2);
this._camera.position.z = 100;
this._camera.lookAt(this._camera.position.x, this._camera.position.y, 0);
this._camera.updateProjectionMatrix();
this._renderer = new THREE.WebGLRenderer({canvas: this._renderingCanvas, alpha:true});
// Add listeners for events that require modifying the scene or camera
this.viewer.addHandler("destroy", ()=>this.destroy());
this.viewer.world.addHandler("add-item", ev => this._addTiledImage(ev));
this.viewer.world.addHandler("remove-item", ev => this._removeTiledImage(ev));
this.viewer.addHandler("tile-ready", ev => this._tileReadyHandler(ev));
this.viewer.addHandler("tile-unloaded", ev => this._tileUnloadedHandler(ev));
this.viewer.addHandler("viewport-change", () => this._viewportChangeHandler());
this.viewer.addHandler("home", () => this._viewportChangeHandler());
// this.viewer.world.addHandler("item-index-change", () => this.renderFrame());
// this.viewer.addHandler("crop-change", () => this.renderFrame());
// this.viewer.addHandler("clip-change", () => this.renderFrame());
// this.viewer.addHandler("opacity-change", () => this.renderFrame());
// this.viewer.addHandler("composite-operation-change", () => this.renderFrame());
this.viewer.addHandler("update-viewport", () => this.renderFrame());
this._viewportChangeHandler();
}
_addTiledImage(event){
let tiledImage = event.item;
// create a Group for the tiles of this tiled image.
if(!tiledImage[this._uuid]){
let tileContainer = new THREE.Group();
let rotationAxis = new THREE.Group();
let positioningGroup = new THREE.Group();
rotationAxis.add(tileContainer);
positioningGroup.add(rotationAxis);
let wrappedCopies = new THREE.Group();
rotationAxis.add(wrappedCopies);
// save mutual references between OpenSceneGraph and ThreeJSRenderer versions of tiledImages
// add unique ID to the tiledImage to look it up in our map in event handlers
let scene = new THREE.Scene()
let light = new THREE.AmbientLight();
scene.add(light);
scene.add(positioningGroup);
scene.userData.tileContainer = tileContainer;
scene.userData.wrappedCopies = wrappedCopies;
let tiledImageID = generateUUID();
tiledImage[this._uuid] = tiledImageID;
this._tiledImageMap[tiledImageID] = scene;
// keep a direct reference to the TiledImage on the Group
positioningGroup._tiledImage = tiledImage;
//offset the tileContainer so the center of the image is at the origin of the parent group
tileContainer.position.x = -0.5;
tileContainer.position.y = -0.5 / tiledImage.source.aspectRatio;
//undo the offset of the tileContainer, moving this back into original viewport coordinate space
rotationAxis.position.x = tileContainer.position.x * -1;
rotationAxis.position.y = tileContainer.position.y * -1;
this._updateTiledImageParameters(tiledImage, positioningGroup, rotationAxis);
tiledImage.addHandler('bounds-change',()=>this._updateTiledImageParameters(tiledImage, positioningGroup, rotationAxis, true));
tiledImage.addHandler('opacity-change',()=>this._updateTiledImageParameters(tiledImage, positioningGroup, rotationAxis, true));
}
this._updateMeshIfNeeded(tiledImage);
this.renderFrame();
}
_removeTiledImage(event){
let tiledImage = event.item;
cleanupObject(this._tiledImageMap[tiledImage[this._uuid]]);
delete this._tiledImageMap[tiledImage[this._uuid]];
this.renderFrame();
}
_tileReadyHandler(event){
let tile = event.tile;
let tiledImage = event.tiledImage;
if(this._tileMap[tile.cacheKey]){
// this tile has already been handled; ignore repeat ready request (happens when image is wrapped)
return;
}
//create a THREE.Material with the image data for this tile
let texture = new THREE.CanvasTexture(event.tile.getCanvasContext().canvas);
texture.flipY = false; // To match OSD reference frame
let material = new THREE.MeshLambertMaterial({
map: texture,
transparent: !!tile.hasTransparency || tiledImage.opacity < 1,
opacity: tiledImage.opacity
});
// cache the material using the tile's cacheKey so that when a tile is located by OpenSeadragon methods, the associate material can be retrieved
this._tileMap[tile.cacheKey] = material;
let numTiles = tiledImage.source.getNumTiles(tile.level);
let tx = OpenSeadragon.positiveModulo(tile.x, numTiles.x);
let ty = OpenSeadragon.positiveModulo(tile.y, numTiles.y);
//cache the bounds for this material so it doesn't have to be recomputed every time it is used to update the scene
material.userData._tileBounds = tiledImage.source.getTileBounds(tile.level, tx, ty);
material.userData.hasTransparency = !!tile.hasTransparency;
material.userData.tile = tile;
material.userData.tiledImage = tiledImage;
//since a new tile is available, update the image (if needed)
this._updateTiledImageRendering(tiledImage, tile);
}
_tileUnloadedHandler(event){
let tile = event.tile;
if(!this._tileMap[tile.cacheKey]){
//already cleaned up
return;
}
this._updateTiledImageRendering(event.tiledImage, tile);
cleanupObject(this._tileMap[tile.cacheKey]);
delete this._tileMap[tile.cacheKey];
}
_updateTiledImageParameters(tiledImage, positioningGroup, rotationAxis, requestRender){
let bounds = tiledImage.getBoundsNoRotate(true);
let rotation = tiledImage.getRotation(true);
//set size and location
positioningGroup.scale.x = bounds.width; //scale the normalized image coordinates to match the size within the world
positioningGroup.scale.y = -bounds.width; //flip Y
positioningGroup.position.x = bounds.x;
positioningGroup.position.y = bounds.y * -1;//flip Y
// rotate about the rotation axis
rotationAxis.rotation.z = rotation * Math.PI / 180;
rotationAxis.scale.x = tiledImage.getFlip() ? -1 : 1;
updateOpacity(this._tiledImageMap[tiledImage[this._uuid]].userData.tileContainer, tiledImage.opacity);
if(requestRender){
this.renderFrame();
}
}
_updateTiledImageRendering(tiledImage, tile){
let scene = this._tiledImageMap[tiledImage[this._uuid]];
let bounds = this._tileMap[tile.cacheKey].userData._tileBounds
if(bounds.x < 0 || bounds.y < 0 || bounds.x >= 1 || bounds.y >= 1){
return;
}
this._updateMeshIfNeeded(tiledImage);
let level = scene.userData.currentLevel;
//whether the tile was just loaded or unloaded, update any tiles that it overlaps in the current tileGrid (as needed)
let topLeft = tiledImage.source.getTileAtPoint(level, {x: bounds.x, y: bounds.y});
let bottomRight = tiledImage.source.getTileAtPoint(level, {x: bounds.x + bounds.width, y: bounds.y + bounds.height});
//iterate over the tiles overlapped by this one
let x, y;
for(x = topLeft.x; x<= bottomRight.x; x++){
for(y = topLeft.y; y <= bottomRight.y; y++){
let mesh = scene.userData._tileMatrix[x][y];
this._loadBestImage(mesh);
}
}
this.renderFrame();
}
_updateMeshIfNeeded(tiledImage){
let tileContainer = this._tiledImageMap[tiledImage[this._uuid]].userData.tileContainer;
let scene = this._tiledImageMap[tiledImage[this._uuid]]
let levelsInterval = tiledImage._getLevelsInterval();
let level = levelsInterval.highestLevel;
if(scene.userData.currentLevel === level){
//we are already drawing the highest-resolution tiles, just return
return;
}
// console.log('new level', level);
scene.userData.currentLevel = level;
//we need to update the grid.
//clear the old matrix
scene.userData._tileMatrix = [];
scene.userData.tiledImage = tiledImage;
//remove existing tiles
tileContainer.children.forEach(cleanupObject);
tileContainer.clear();
//create new set of tiles and add to the tileContainer
let gridInfo = tiledImage.getGridDefinition(level);
let col, row;
for(col = 0; col < gridInfo.numColumns; col += 1){
scene.userData._tileMatrix[col] = [];
for(row = 0; row < gridInfo.numRows; row += 1){
let colInfo = gridInfo.columnInfo[col];
let rowInfo = gridInfo.rowInfo[row];
let left = colInfo.x;
let top = rowInfo.y;
let x = left + colInfo.width / 2;
let y = top + rowInfo.height / 2;
let z = 0;
let tileGeometry = new THREE.PlaneGeometry(colInfo.width, rowInfo.height);
let mesh = new THREE.Mesh(tileGeometry);
mesh.position.set(x, y, z);
mesh.userData._tileInfo = {
row: row,
col: col,
level: level,
...rowInfo,
...colInfo,
tiledImageID: tiledImage[this._uuid],
center: new OpenSeadragon.Point(x, y),
uvMapOriginal: [],
// uvMap
}
let i;
let uvAttribute = tileGeometry.attributes.uv;
for(i =0 ; i<uvAttribute.count; ++i){
let x = uvAttribute.getX(i);
let y = uvAttribute.getY(i);
mesh.userData._tileInfo.uvMapOriginal[i] = [x, y];
}
tileContainer.add(mesh);
scene.userData._tileMatrix[col][row] = mesh;
//get (current) best image data for the tile
this._loadBestImage(mesh);
}
}
}
_loadBestImage(mesh){
let tileInfo = mesh.userData._tileInfo;
let tiledImage = this._tiledImageMap[tileInfo.tiledImageID].userData.tiledImage;
let tileSource = tiledImage.source;
let tilesMatrix = tiledImage.tilesMatrix;
//if we have our own texture, use it
// let tile = tilesMatrix[tileInfo.level][tileInfo.col][tileInfo.row];
let tile = getTile(tilesMatrix, tileInfo.level, tileInfo.col, tileInfo.row);
let material = tile && this._tileMap[tile.cacheKey];
if(material){
addMaterialToMesh(mesh, material, tiledImage.opacity);
} else {
//start at next highest level and work downward
let queryLevel = tileInfo.level - 1;
while(queryLevel >= 0){
let tileIndex = tileSource.getTileAtPoint(queryLevel, tileInfo.center);
let tile = getTile(tilesMatrix, queryLevel, tileIndex.x, tileIndex.y);
let material = tile && this._tileMap[tile.cacheKey];
if(material){
addMaterialToMesh(mesh, material, tiledImage.opacity);
break;
}
queryLevel--;
}
}
}
_viewportChangeHandler(){
let viewer = this.viewer;
let viewerBounds = viewer.viewport.getBoundsNoRotate(true);
this._camera.left = viewerBounds.width / -2;
this._camera.right = viewerBounds.width / 2;
this._camera.top = viewerBounds.height / 2;
this._camera.bottom = viewerBounds.height / -2;
let center = viewer.viewport.getCenter(true);
this._camera.position.x = center.x;
this._camera.position.y = -center.y;
this._camera.rotation.z = viewer.viewport.getRotation(true) * Math.PI / 180;
this._camera.updateProjectionMatrix();
let numItems = viewer.world.getItemCount();
let i;
for(i = 0; i < numItems; i++){
let tiledImage = viewer.world.getItemAt(i);
this._updateMeshIfNeeded(tiledImage);
}
this.renderFrame();
}
_renderToClippingCanvas(item){
let _this = this;
this._clippingContext.clearRect(0, 0, this._clippingCanvas.width, this._clippingCanvas.height);
this._clippingContext.save();
if(item._clip){
var box = item.imageToViewportRectangle(item._clip, true);
var rect = this.viewportToDrawerRectangle(box);
this._clippingContext.beginPath();
this._clippingContext.rect(rect.x, rect.y, rect.width, rect.height);
this._clippingContext.clip();
}
if(item._croppingPolygons){
let polygons = item._croppingPolygons.map(function (polygon) {
return polygon.map(function (coord) {
let point = item.imageToViewportCoordinates(coord.x, coord.y, true);
let clipPoint = _this.viewportCoordToDrawerCoord(point);
return clipPoint;
});
});
this._clippingContext.beginPath();
polygons.forEach(function (polygon) {
polygon.forEach(function (coord, i) {
_this._clippingContext[i === 0 ? 'moveTo' : 'lineTo'](coord.x, coord.y);
});
});
this._clippingContext.clip();
}
this._clippingContext.drawImage(this._renderingCanvas, 0, 0);
this._clippingContext.restore();
}
// private
_offsetForRotation(options) {
var point = options.point ?
options.point.times(OpenSeadragon.pixelDensityRatio) :
new OpenSeadragon.Point(this._outputCanvas.width / 2, this._outputCanvas.height / 2);
var context = this._outputContext;
context.save();
context.translate(point.x, point.y);
if(this.viewport.flipped){
context.rotate(Math.PI / 180 * -options.degrees);
context.scale(-1, 1);
} else{
context.rotate(Math.PI / 180 * options.degrees);
}
context.translate(-point.x, -point.y);
}
// private
_drawDebugInfoOnTile(tile, count, i, tiledImage) {
var colorIndex = this.viewer.world.getIndexOfItem(tiledImage) % this.debugGridColor.length;
var context = this._outputContext;
context.save();
context.lineWidth = 2 * OpenSeadragon.pixelDensityRatio;
context.font = 'small-caps bold ' + (13 * OpenSeadragon.pixelDensityRatio) + 'px arial';
context.strokeStyle = this.debugGridColor[colorIndex];
context.fillStyle = this.debugGridColor[colorIndex];
if (this.viewport.getRotation(true) % 360 !== 0 ) {
this._offsetForRotation({degrees: this.viewport.getRotation(true)});
}
if (tiledImage.getRotation(true) % 360 !== 0) {
this._offsetForRotation({
degrees: tiledImage.getRotation(true),
point: tiledImage.viewport.pixelFromPointNoRotate(
tiledImage._getRotationPoint(true), true)
});
}
if (tiledImage.viewport.getRotation(true) % 360 === 0 &&
tiledImage.getRotation(true) % 360 === 0) {
if(tiledImage._drawer.viewer.viewport.getFlip()) {
tiledImage._drawer._flip();
}
}
context.strokeRect(
tile.position.x * OpenSeadragon.pixelDensityRatio,
tile.position.y * OpenSeadragon.pixelDensityRatio,
tile.size.x * OpenSeadragon.pixelDensityRatio,
tile.size.y * OpenSeadragon.pixelDensityRatio
);
var tileCenterX = (tile.position.x + (tile.size.x / 2)) * OpenSeadragon.pixelDensityRatio;
var tileCenterY = (tile.position.y + (tile.size.y / 2)) * OpenSeadragon.pixelDensityRatio;
// Rotate the text the right way around.
context.translate( tileCenterX, tileCenterY );
context.rotate( Math.PI / 180 * -this.viewport.getRotation(true) );
context.translate( -tileCenterX, -tileCenterY );
if( tile.x === 0 && tile.y === 0 ){
context.fillText(
"Zoom: " + this.viewport.getZoom(),
tile.position.x * OpenSeadragon.pixelDensityRatio,
(tile.position.y - 30) * OpenSeadragon.pixelDensityRatio
);
context.fillText(
"Pan: " + this.viewport.getBounds().toString(),
tile.position.x * OpenSeadragon.pixelDensityRatio,
(tile.position.y - 20) * OpenSeadragon.pixelDensityRatio
);
}
context.fillText(
"Level: " + tile.level,
(tile.position.x + 10) * OpenSeadragon.pixelDensityRatio,
(tile.position.y + 20) * OpenSeadragon.pixelDensityRatio
);
context.fillText(
"Column: " + tile.x,
(tile.position.x + 10) * OpenSeadragon.pixelDensityRatio,
(tile.position.y + 30) * OpenSeadragon.pixelDensityRatio
);
context.fillText(
"Row: " + tile.y,
(tile.position.x + 10) * OpenSeadragon.pixelDensityRatio,
(tile.position.y + 40) * OpenSeadragon.pixelDensityRatio
);
context.fillText(
"Order: " + i + " of " + count,
(tile.position.x + 10) * OpenSeadragon.pixelDensityRatio,
(tile.position.y + 50) * OpenSeadragon.pixelDensityRatio
);
context.fillText(
"Size: " + tile.size.toString(),
(tile.position.x + 10) * OpenSeadragon.pixelDensityRatio,
(tile.position.y + 60) * OpenSeadragon.pixelDensityRatio
);
context.fillText(
"Position: " + tile.position.toString(),
(tile.position.x + 10) * OpenSeadragon.pixelDensityRatio,
(tile.position.y + 70) * OpenSeadragon.pixelDensityRatio
);
if (this.viewport.getRotation(true) % 360 !== 0 ) {
this._restoreRotationChanges();
}
if (tiledImage.getRotation(true) % 360 !== 0) {
this._restoreRotationChanges();
}
if (tiledImage.viewport.getRotation(true) % 360 === 0 &&
tiledImage.getRotation(true) % 360 === 0) {
if(tiledImage._drawer.viewer.viewport.getFlip()) {
tiledImage._drawer._flip();
}
}
context.restore();
}
_drawDebugInfo( tiledImage ) {
let scene = this._tiledImageMap[tiledImage[this._uuid]];
let level = scene.userData.currentLevel;
let tiles = tiledImage.getTileInfoForDrawing().filter(tile=>tile.level === level);
// only draw on the highest level tiles
for ( var i = tiles.length - 1; i >= 0; i-- ) {
var tile = tiles[ i ].tile;
try {
this._drawDebugInfoOnTile(tile, tiles.length, i, tiledImage);
} catch(e) {
OpenSeadragon.console.error(e);
}
}
}
// private
_debugRect(rect) {
if ( this.useCanvas ) {
var context = this._outputContext;
context.save();
context.lineWidth = 2 * OpenSeadragon.pixelDensityRatio;
context.strokeStyle = this.debugGridColor[0];
context.fillStyle = this.debugGridColor[0];
context.strokeRect(
rect.x * OpenSeadragon.pixelDensityRatio,
rect.y * OpenSeadragon.pixelDensityRatio,
rect.width * OpenSeadragon.pixelDensityRatio,
rect.height * OpenSeadragon.pixelDensityRatio
);
context.restore();
}
}
// private
_restoreRotationChanges() {
var context = this._outputContext;
context.restore();
}
}
// Functions below do not depend on an instance of the Drawer, and can be defined outside of the class definition
function updateOpacity(meshGroup, opacity){
meshGroup.children.forEach(mesh=>{
mesh.material.opacity = opacity;
if(opacity < 1 || mesh.material.userData.hasTransparency){
mesh.material.transparent = true;
} else {
mesh.material.transparent = false;
}
})
}
function getTile(tileMatrix, level, x, y){
return tileMatrix[level] && tileMatrix[level][x] && tileMatrix[level][x][y];
}
function addMaterialToMesh(mesh, material, opacity){
let regionInfo = mesh.userData._tileInfo;
let materialBounds = material.userData._tileBounds;
//update transparent and opacity properties to reflect current state
let transparent = opacity < 1 || material.userData.hasTransparency;
material.transparent = transparent;
material.opacity = opacity;
mesh.material = material;
let uvMap = mesh.userData._tileInfo.uvMapOriginal;
let uvAttribute = mesh.geometry.attributes.uv;
// iterate over UV map for each vertex and calculate position within material/texture
let xNew, yNew;
let regionLeft = regionInfo.x;
let regionTop = regionInfo.y;
let regionRight = regionLeft + regionInfo.width;
let regionBottom = regionTop + regionInfo.height;
//what is needed to calculate the right uv index for each vertex?
// 1) position of the vertex in normalized coordinates
// 2) position of the entire texture area (not just non-overlapped area) in normalized coordinates
uvMap.forEach(([x,y],i)=>{
// x, y describe which corner of the original texture to use
if(x==0){
xNew = (regionLeft - materialBounds.x) / materialBounds.width;
} else {
xNew = (regionRight - materialBounds.x) / materialBounds.width;
}
if(y == 0){
yNew = (regionTop - materialBounds.y) / materialBounds.height;
} else {
yNew = (regionBottom - materialBounds.y) / materialBounds.height;
}
uvAttribute.setXY(i, xNew, yNew);
});
uvAttribute.needsUpdate = true;
}
function createWrappingGrid(scene, tiledImage){
let container = scene.userData.wrappedCopies;
container.clear();
//calculate how to tile the space
let tiledImageBounds = tiledImage.getBounds();
let imgBounds = {x: 0, y: 0, width: tiledImageBounds.width, height: tiledImageBounds.height };
let drawArea = tiledImage.viewer.viewport.getBounds(true);
let center = drawArea.getCenter();
let halfDiag = Math.sqrt(drawArea.width * drawArea.width + drawArea.height * drawArea.height);
let extraBuffer = imgBounds.width + imgBounds.height;
let left = center.x - halfDiag - extraBuffer;
let right = center.x + halfDiag + extraBuffer;
let top = center.y - halfDiag - extraBuffer;
let bottom = center.y + halfDiag + extraBuffer;
let xMin = tiledImage.wrapHorizontal ? Math.floor(left / imgBounds.width) : imgBounds.x;
let yMin = tiledImage.wrapVertical ? Math.floor(top / imgBounds.height) : imgBounds.y;
let xMax = tiledImage.wrapHorizontal ? Math.floor(right / imgBounds.width) : imgBounds.x;
let yMax = tiledImage.wrapVertical ? Math.floor(bottom / imgBounds.height) : imgBounds.y;
for(let x = xMin; x <= xMax; x += 1){
for(let y = yMin; y <= yMax; y += 1){
if(x == 0 && y == 0) {
continue;
}
let clone = scene.userData.tileContainer.clone();
clone.position.x += x;
clone.position.y += y;
container.add(clone);
}
}
}
function cleanupObject(object){
if(object.children && object.children.forEach){
object.children.forEach(cleanupObject);
}
if(object.dispose){
object.dispose();
}
if(object.geometry){
object.geometry.dispose();
}
}
// http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/21963136#21963136
const _lut = [ '00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '0a', '0b', '0c', '0d', '0e', '0f', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '1a', '1b', '1c', '1d', '1e', '1f', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '2a', '2b', '2c', '2d', '2e', '2f', '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '3a', '3b', '3c', '3d', '3e', '3f', '40', '41', '42', '43', '44', '45', '46', '47', '48', '49', '4a', '4b', '4c', '4d', '4e', '4f', '50', '51', '52', '53', '54', '55', '56', '57', '58', '59', '5a', '5b', '5c', '5d', '5e', '5f', '60', '61', '62', '63', '64', '65', '66', '67', '68', '69', '6a', '6b', '6c', '6d', '6e', '6f', '70', '71', '72', '73', '74', '75', '76', '77', '78', '79', '7a', '7b', '7c', '7d', '7e', '7f', '80', '81', '82', '83', '84', '85', '86', '87', '88', '89', '8a', '8b', '8c', '8d', '8e', '8f', '90', '91', '92', '93', '94', '95', '96', '97', '98', '99', '9a', '9b', '9c', '9d', '9e', '9f', 'a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8', 'a9', 'aa', 'ab', 'ac', 'ad', 'ae', 'af', 'b0', 'b1', 'b2', 'b3', 'b4', 'b5', 'b6', 'b7', 'b8', 'b9', 'ba', 'bb', 'bc', 'bd', 'be', 'bf', 'c0', 'c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9', 'ca', 'cb', 'cc', 'cd', 'ce', 'cf', 'd0', 'd1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7', 'd8', 'd9', 'da', 'db', 'dc', 'dd', 'de', 'df', 'e0', 'e1', 'e2', 'e3', 'e4', 'e5', 'e6', 'e7', 'e8', 'e9', 'ea', 'eb', 'ec', 'ed', 'ee', 'ef', 'f0', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'fa', 'fb', 'fc', 'fd', 'fe', 'ff' ];
function generateUUID() {
const d0 = Math.random() * 0xffffffff | 0;
const d1 = Math.random() * 0xffffffff | 0;
const d2 = Math.random() * 0xffffffff | 0;
const d3 = Math.random() * 0xffffffff | 0;
const uuid = _lut[ d0 & 0xff ] + _lut[ d0 >> 8 & 0xff ] + _lut[ d0 >> 16 & 0xff ] + _lut[ d0 >> 24 & 0xff ] + '-' +
_lut[ d1 & 0xff ] + _lut[ d1 >> 8 & 0xff ] + '-' + _lut[ d1 >> 16 & 0x0f | 0x40 ] + _lut[ d1 >> 24 & 0xff ] + '-' +
_lut[ d2 & 0x3f | 0x80 ] + _lut[ d2 >> 8 & 0xff ] + '-' + _lut[ d2 >> 16 & 0xff ] + _lut[ d2 >> 24 & 0xff ] +
_lut[ d3 & 0xff ] + _lut[ d3 >> 8 & 0xff ] + _lut[ d3 >> 16 & 0xff ] + _lut[ d3 >> 24 & 0xff ];
// .toLowerCase() here flattens concatenated strings to save heap memory space.
return uuid.toLowerCase();
}

View File

@ -1,841 +0,0 @@
// import 'https://cdnjs.cloudflare.com/ajax/libs/three.js/0.149.0/three.min.js';
import '../lib/three.js';
const THREE = window.THREE;
const DEPTH_MULTIPLIER = 0.1;
export class ThreeJSRenderer extends OpenSeadragon.Drawer{
constructor(openSeadragonViewer, canvas){
this._viewer = openSeadragonViewer;
this._camera = null;
this._scene = null;
this._imageContainer = null;
this._renderer = null;
this._renderingContinously = false;
this._animationFrame = null;
if(canvas){
this._canvas = canvas;
} else {
let viewerCanvas = viewer.drawer.canvas;
this._canvas = viewer.drawer.canvas;
// let canvas = this._canvas = document.createElement('canvas');
// canvas.insertBefore(viewerCanvas);
// canvas.style.width = viewerCanvas.clientWidth+'px';
// canvas.style.height = viewerCanvas.clientHeight+'px';
// canvas.width = viewerCanvas.width;
// canvas.height = viewerCanvas.height;
// //make the test canvas mirror all changes to the viewer canvas
// viewer.addHandler("resize", function(){
// canvas.style.width = viewerCanvas.clientWidth+'px';
// canvas.style.height = viewerCanvas.clientHeight+'px';
// });
}
createThreeViewer(this);
}
renderFrame(){
if(this._animationFrame) {
cancelAnimationFrame(this._animationFrame);
}
this._animationFrame = requestAnimationFrame(()=>this.render());
}
render(){
// this.camera.updateProjectionMatrix();
this._renderer.render(this._scene, this._camera);
this._animationFrame = null;
if(this._renderingContinuously){
this.renderFrame();
}
}
renderContinuously(continuously){
if(continuously){
this._renderingContinuously = true;
} else {
this._renderingContinuously = false;
}
}
destroy(){
//clear all resources used by the renderer, geometries, textures etc
//to do: remove handlers from viewer and dispose of any remaining textures/materials
cleanupObject(this._scene);
cleanupObject(this._renderer);
cleanupObject(this._camera);
this._scene = null;
this._renderer = null;
this._camera = null;
}
//// Override API from OpenSeadragon.Drawer
/**
* This function will create multiple polygon paths on the drawing context by provided polygons,
* then clip the context to the paths.
* @param {OpenSeadragon.Point[][]} polygons - an array of polygons. A polygon is an array of OpenSeadragon.Point
* @param {Boolean} useSketch - Whether to use the sketch canvas or not.
*/
clipWithPolygons(polygons, useSketch) {
if (!this.useCanvas) {
return;
}
var context = this._getContext(useSketch);
context.beginPath();
polygons.forEach(function (polygon) {
polygon.forEach(function (coord, i) {
context[i === 0 ? 'moveTo' : 'lineTo'](coord.x, coord.y);
});
});
context.clip();
}
/**
* Destroy the drawer (unload current loaded tiles)
*/
destroy() {
//force unloading of current canvas (1x1 will be gc later, trick not necessarily needed)
this.canvas.width = 1;
this.canvas.height = 1;
this.sketchCanvas = null;
this.sketchContext = null;
}
/**
* Clears the Drawer so it's ready to draw another frame.
*/
clear(){
this.canvas.innerHTML = "";
if ( this.useCanvas ) {
var viewportSize = this._calculateCanvasSize();
if( this.canvas.width !== viewportSize.x ||
this.canvas.height !== viewportSize.y ) {
this.canvas.width = viewportSize.x;
this.canvas.height = viewportSize.y;
this._updateImageSmoothingEnabled(this.context);
if ( this.sketchCanvas !== null ) {
var sketchCanvasSize = this._calculateSketchCanvasSize();
this.sketchCanvas.width = sketchCanvasSize.x;
this.sketchCanvas.height = sketchCanvasSize.y;
this._updateImageSmoothingEnabled(this.sketchContext);
}
}
this._clear();
}
}
_clear(useSketch, bounds) {
if (!this.useCanvas) {
return;
}
var context = this._getContext(useSketch);
if (bounds) {
context.clearRect(bounds.x, bounds.y, bounds.width, bounds.height);
} else {
var canvas = context.canvas;
context.clearRect(0, 0, canvas.width, canvas.height);
}
}
/**
* Draws the given tile.
* @param {OpenSeadragon.Tile} tile - The tile to draw.
* @param {Function} drawingHandler - Method for firing the drawing event if using canvas.
* drawingHandler({context, tile, rendered})
* @param {Boolean} useSketch - Whether to use the sketch canvas or not.
* where <code>rendered</code> is the context with the pre-drawn image.
* @param {Float} [scale=1] - Apply a scale to tile position and size. Defaults to 1.
* @param {OpenSeadragon.Point} [translate] A translation vector to offset tile position
* @param {Boolean} [shouldRoundPositionAndSize] - Tells whether to round
* position and size of tiles supporting alpha channel in non-transparency
* context.
* @param {OpenSeadragon.TileSource} source - The source specification of the tile.
*/
drawTile( tile, drawingHandler, useSketch, scale, translate, shouldRoundPositionAndSize, source) {
$.console.assert(tile, '[Drawer.drawTile] tile is required');
$.console.assert(drawingHandler, '[Drawer.drawTile] drawingHandler is required');
if (this.useCanvas) {
var context = this._getContext(useSketch);
scale = scale || 1;
tile.drawCanvas(context, drawingHandler, scale, translate, shouldRoundPositionAndSize, source);
} else {
tile.drawHTML( this.canvas );
}
}
_getContext( useSketch ) {
var context = this.context;
if ( useSketch ) {
if (this.sketchCanvas === null) {
this.sketchCanvas = document.createElement( "canvas" );
var sketchCanvasSize = this._calculateSketchCanvasSize();
this.sketchCanvas.width = sketchCanvasSize.x;
this.sketchCanvas.height = sketchCanvasSize.y;
this.sketchContext = this.sketchCanvas.getContext( "2d" );
// If the viewport is not currently rotated, the sketchCanvas
// will have the same size as the main canvas. However, if
// the viewport get rotated later on, we will need to resize it.
if (this.viewport.getRotation() === 0) {
var self = this;
this.viewer.addHandler('rotate', function resizeSketchCanvas() {
if (self.viewport.getRotation() === 0) {
return;
}
self.viewer.removeHandler('rotate', resizeSketchCanvas);
var sketchCanvasSize = self._calculateSketchCanvasSize();
self.sketchCanvas.width = sketchCanvasSize.x;
self.sketchCanvas.height = sketchCanvasSize.y;
});
}
this._updateImageSmoothingEnabled(this.sketchContext);
}
context = this.sketchContext;
}
return context;
}
// private
saveContext() {
if (!this.useCanvas) {
return;
}
this._getContext( useSketch ).save();
}
// private
restoreContext( useSketch ) {
if (!this.useCanvas) {
return;
}
this._getContext( useSketch ).restore();
}
// private
setClip(rect, useSketch) {
if (!this.useCanvas) {
return;
}
var context = this._getContext( useSketch );
context.beginPath();
context.rect(rect.x, rect.y, rect.width, rect.height);
context.clip();
}
// private
drawRectangle(rect, fillStyle, useSketch) {
if (!this.useCanvas) {
return;
}
var context = this._getContext( useSketch );
context.save();
context.fillStyle = fillStyle;
context.fillRect(rect.x, rect.y, rect.width, rect.height);
context.restore();
}
/**
* Blends the sketch canvas in the main canvas.
* @param {Object} options The options
* @param {Float} options.opacity The opacity of the blending.
* @param {Float} [options.scale=1] The scale at which tiles were drawn on
* the sketch. Default is 1.
* Use scale to draw at a lower scale and then enlarge onto the main canvas.
* @param {OpenSeadragon.Point} [options.translate] A translation vector
* that was used to draw the tiles
* @param {String} [options.compositeOperation] - How the image is
* composited onto other images; see compositeOperation in
* {@link OpenSeadragon.Options} for possible values.
* @param {OpenSeadragon.Rect} [options.bounds] The part of the sketch
* canvas to blend in the main canvas. If specified, options.scale and
* options.translate get ignored.
*/
blendSketch(opacity, scale, translate, compositeOperation) {
var options = opacity;
if (!$.isPlainObject(options)) {
options = {
opacity: opacity,
scale: scale,
translate: translate,
compositeOperation: compositeOperation
};
}
if (!this.useCanvas || !this.sketchCanvas) {
return;
}
opacity = options.opacity;
compositeOperation = options.compositeOperation;
var bounds = options.bounds;
this.context.save();
this.context.globalAlpha = opacity;
if (compositeOperation) {
this.context.globalCompositeOperation = compositeOperation;
}
if (bounds) {
// Internet Explorer, Microsoft Edge, and Safari have problems
// when you call context.drawImage with negative x or y
// or x + width or y + height greater than the canvas width or height respectively.
if (bounds.x < 0) {
bounds.width += bounds.x;
bounds.x = 0;
}
if (bounds.x + bounds.width > this.canvas.width) {
bounds.width = this.canvas.width - bounds.x;
}
if (bounds.y < 0) {
bounds.height += bounds.y;
bounds.y = 0;
}
if (bounds.y + bounds.height > this.canvas.height) {
bounds.height = this.canvas.height - bounds.y;
}
this.context.drawImage(
this.sketchCanvas,
bounds.x,
bounds.y,
bounds.width,
bounds.height,
bounds.x,
bounds.y,
bounds.width,
bounds.height
);
} else {
scale = options.scale || 1;
translate = options.translate;
var position = translate instanceof $.Point ?
translate : new $.Point(0, 0);
var widthExt = 0;
var heightExt = 0;
if (translate) {
var widthDiff = this.sketchCanvas.width - this.canvas.width;
var heightDiff = this.sketchCanvas.height - this.canvas.height;
widthExt = Math.round(widthDiff / 2);
heightExt = Math.round(heightDiff / 2);
}
this.context.drawImage(
this.sketchCanvas,
position.x - widthExt * scale,
position.y - heightExt * scale,
(this.canvas.width + 2 * widthExt) * scale,
(this.canvas.height + 2 * heightExt) * scale,
-widthExt,
-heightExt,
this.canvas.width + 2 * widthExt,
this.canvas.height + 2 * heightExt
);
}
this.context.restore();
}
// private
drawDebugInfo(tile, count, i, tiledImage) {
if ( !this.useCanvas ) {
return;
}
var colorIndex = this.viewer.world.getIndexOfItem(tiledImage) % this.debugGridColor.length;
var context = this.context;
context.save();
context.lineWidth = 2 * $.pixelDensityRatio;
context.font = 'small-caps bold ' + (13 * $.pixelDensityRatio) + 'px arial';
context.strokeStyle = this.debugGridColor[colorIndex];
context.fillStyle = this.debugGridColor[colorIndex];
if (this.viewport.getRotation(true) % 360 !== 0 ) {
this._offsetForRotation({degrees: this.viewport.getRotation(true)});
}
if (tiledImage.getRotation(true) % 360 !== 0) {
this._offsetForRotation({
degrees: tiledImage.getRotation(true),
point: tiledImage.viewport.pixelFromPointNoRotate(
tiledImage._getRotationPoint(true), true)
});
}
if (tiledImage.viewport.getRotation(true) % 360 === 0 &&
tiledImage.getRotation(true) % 360 === 0) {
if(tiledImage._drawer.viewer.viewport.getFlip()) {
tiledImage._drawer._flip();
}
}
context.strokeRect(
tile.position.x * $.pixelDensityRatio,
tile.position.y * $.pixelDensityRatio,
tile.size.x * $.pixelDensityRatio,
tile.size.y * $.pixelDensityRatio
);
var tileCenterX = (tile.position.x + (tile.size.x / 2)) * $.pixelDensityRatio;
var tileCenterY = (tile.position.y + (tile.size.y / 2)) * $.pixelDensityRatio;
// Rotate the text the right way around.
context.translate( tileCenterX, tileCenterY );
context.rotate( Math.PI / 180 * -this.viewport.getRotation(true) );
context.translate( -tileCenterX, -tileCenterY );
if( tile.x === 0 && tile.y === 0 ){
context.fillText(
"Zoom: " + this.viewport.getZoom(),
tile.position.x * $.pixelDensityRatio,
(tile.position.y - 30) * $.pixelDensityRatio
);
context.fillText(
"Pan: " + this.viewport.getBounds().toString(),
tile.position.x * $.pixelDensityRatio,
(tile.position.y - 20) * $.pixelDensityRatio
);
}
context.fillText(
"Level: " + tile.level,
(tile.position.x + 10) * $.pixelDensityRatio,
(tile.position.y + 20) * $.pixelDensityRatio
);
context.fillText(
"Column: " + tile.x,
(tile.position.x + 10) * $.pixelDensityRatio,
(tile.position.y + 30) * $.pixelDensityRatio
);
context.fillText(
"Row: " + tile.y,
(tile.position.x + 10) * $.pixelDensityRatio,
(tile.position.y + 40) * $.pixelDensityRatio
);
context.fillText(
"Order: " + i + " of " + count,
(tile.position.x + 10) * $.pixelDensityRatio,
(tile.position.y + 50) * $.pixelDensityRatio
);
context.fillText(
"Size: " + tile.size.toString(),
(tile.position.x + 10) * $.pixelDensityRatio,
(tile.position.y + 60) * $.pixelDensityRatio
);
context.fillText(
"Position: " + tile.position.toString(),
(tile.position.x + 10) * $.pixelDensityRatio,
(tile.position.y + 70) * $.pixelDensityRatio
);
if (this.viewport.getRotation(true) % 360 !== 0 ) {
this._restoreRotationChanges();
}
if (tiledImage.getRotation(true) % 360 !== 0) {
this._restoreRotationChanges();
}
if (tiledImage.viewport.getRotation(true) % 360 === 0 &&
tiledImage.getRotation(true) % 360 === 0) {
if(tiledImage._drawer.viewer.viewport.getFlip()) {
tiledImage._drawer._flip();
}
}
context.restore();
}
// private
debugRect(rect) {
if ( this.useCanvas ) {
var context = this.context;
context.save();
context.lineWidth = 2 * $.pixelDensityRatio;
context.strokeStyle = this.debugGridColor[0];
context.fillStyle = this.debugGridColor[0];
context.strokeRect(
rect.x * $.pixelDensityRatio,
rect.y * $.pixelDensityRatio,
rect.width * $.pixelDensityRatio,
rect.height * $.pixelDensityRatio
);
context.restore();
}
}
}
/*** Create THREE.js version of rendering tiled images using WebGL ****/
function createThreeViewer(instance){
// Add listeners for events that require modifying the scene or camera
instance._viewer.addHandler("close", ()=>instance.destroy());
instance._viewer.world.addHandler("add-item", ev => addTiledImage(ev, instance));
instance._viewer.world.addHandler("remove-item", ev => removeTiledImage(ev, instance));
instance._viewer.world.addHandler("item-index-change", ev => setItemOrder(ev, instance));
instance._viewer.addHandler("tile-ready", ev => tileReady(ev, instance));
instance._viewer.addHandler("tile-unloaded", ev => tileUnloaded(ev, instance));
instance._viewer.addHandler("viewport-change", ev => viewportChange(ev, instance));
// instance._viewer.addHandler("update-viewport", ev => instance.renderFrame());
//to do: support pages of sequence mode
//to do: add handler for resize event to auto-sync
let viewerBounds = instance._viewer.viewport.getBoundsNoRotate();
instance._scene = new THREE.Scene();
instance._imageContainer = new THREE.Group();
instance._camera = new THREE.OrthographicCamera(
viewerBounds.width / -2,
viewerBounds.width / 2,
viewerBounds.height / 2,
viewerBounds.height / -2,
0,
10000
);
instance._camera.position.x = viewerBounds.x + viewerBounds.width/2;
instance._camera.position.y = -(viewerBounds.y + viewerBounds.height/2);
instance._camera.position.z = 100;
instance._camera.lookAt(instance._camera.position.x, instance._camera.position.y, 0);
instance._camera.updateProjectionMatrix();
var light = new THREE.AmbientLight();
instance._scene.add(light);
instance._scene.add(instance._imageContainer);
instance._scene.background = new THREE.Color(0.25, 0.25, 0.25);
instance._renderer = new THREE.WebGLRenderer({canvas: instance._canvas});
}
//private
function tileReady(event, instance){
let tile = event.tile;
let tiledImage = event.tiledImage;
//create a THREE.Material with the image data for this tile
let texture = new THREE.CanvasTexture(event.tile.getCanvasContext().canvas);
texture.flipY = false; // To match OSD reference frame
let material = new THREE.MeshLambertMaterial({
map: texture,
transparent: !!tile.hasTransparency || tiledImage.opacity < 1,
opacity: tiledImage.opacity
});
//attach the material to the tile so it can be queried by location using OpenSeadragon methods
tile._three = material;
//cache the bounds for this material so it doesn't have to be recomputed every time it is used to update the scene
material.userData._tileBounds = tiledImage.source.getTileBounds(tile.level, tile.x, tile.y);
material.userData.hasTransparency = !!tile.hasTransparency;
material.userData.tile = tile;
material.userData.tiledImage = tiledImage;
//since a new tile is available, update the image (if needed)
updateTiledImageRendering(tiledImage, tile, instance);
}
function tileUnloaded(event, instance){
let tile = event.tile;
cleanupObject(tile._three);
delete tile._three;
updateTiledImageRendering(event.tiledImage, tile, instance);
}
function viewportChange(event, instance){
let viewer = event.eventSource;
let viewerBounds = viewer.viewport.getBoundsNoRotate(true);
instance._camera.left = viewerBounds.width / -2;
instance._camera.right = viewerBounds.width / 2;
instance._camera.top = viewerBounds.height / 2;
instance._camera.bottom = viewerBounds.height / -2;
let center = viewer.viewport.getCenter(true);
instance._camera.position.x = center.x;
instance._camera.position.y = -center.y;
instance._camera.rotation.z = viewer.viewport.getRotation(true) * Math.PI / 180;
instance._camera.updateProjectionMatrix();
let numItems = viewer.world.getItemCount();
let i;
for(i = 0; i < numItems; i++){
let tiledImage = viewer.world.getItemAt(i);
updateMeshIfNeeded(tiledImage);
}
instance.renderFrame();
}
function addTiledImage(event, instance){
let tiledImage = event.item;
//create a Group for the tiles of this tiled image.
if(!tiledImage._three){
let tileContainer = new THREE.Group();
let rotationAxis = new THREE.Group();
let positioningGroup = new THREE.Group();
rotationAxis.add(tileContainer);
positioningGroup.add(rotationAxis);
positioningGroup.userData._tileContainer = tileContainer;
//add the object to the group of images (i.e. add to the scene)
instance._imageContainer.add(positioningGroup);
//save mutual references between OpenSceneGraph and ThreeJSRenderer versions of tiledImages
tiledImage._three = positioningGroup;
positioningGroup._tiledImage = tiledImage;
//offset the tileContainer so the center of the image is at the origin of the parent group
tileContainer.position.x = -0.5;
tileContainer.position.y = -0.5 / tiledImage.source.aspectRatio;
//undo the offset of the tileContainer, moving this back into original viewport coordinate space
rotationAxis.position.x = tileContainer.position.x * -1;
rotationAxis.position.y = tileContainer.position.y * -1;
updateTiledImageParameters(instance, tiledImage, positioningGroup, rotationAxis);
tiledImage.addHandler('bounds-change',()=>updateTiledImageParameters(instance, tiledImage, positioningGroup, rotationAxis, true));
tiledImage.addHandler('opacity-change',()=>updateTiledImageParameters(instance, tiledImage, positioningGroup, rotationAxis, true));
}
setItemOrder(null, instance);
}
function removeTiledImage(event){
let tiledImage = event.item;
//to do: make sure all resources for all tiles are unloaded (even if not actively in the tile group)
tiledImage._three.removeFromParent();
cleanupObject(tiledImage._three);
delete tiledImage._three;
}
function updateTiledImageParameters(instance, tiledImage, positioningGroup, rotationAxis, requestRender){
let bounds = tiledImage.getBoundsNoRotate(true);
let rotation = tiledImage.getRotation(true);
//set size and location
positioningGroup.scale.x = bounds.width; //scale the normalized image coordinates to match the size within the world
positioningGroup.scale.y = -bounds.width; //flip Y
positioningGroup.position.x = bounds.x;
positioningGroup.position.y = bounds.y * -1;//flip Y
// rotate about the rotation axis
rotationAxis.rotation.z = rotation * Math.PI / 180;
rotationAxis.scale.x = tiledImage.getFlip() ? -1 : 1;
updateOpacity(tiledImage._three.userData._tileContainer, tiledImage.opacity);
if(requestRender){
instance.renderFrame();
}
}
function updateOpacity(meshGroup, opacity){
meshGroup.children.forEach(mesh=>{
mesh.material.opacity = opacity;
if(opacity < 1 || mesh.material.userData.hasTransparency){
mesh.material.transparent = true;
} else {
mesh.material.transparent = false;
}
})
}
function updateTiledImageRendering(tiledImage, tile, instance){
updateMeshIfNeeded(tiledImage);
let tileContainer = tiledImage._three.userData._tileContainer;
let level = tileContainer.userData._tiledImageLevel;
//whether the tile was just loaded or unloaded, update any tiles that it overlaps in the current tileGrid (as needed)
let topLeft = tiledImage.source.getTileAtPoint(level, {x: tile.bounds.x, y: tile.bounds.y});
let bottomRight = tiledImage.source.getTileAtPoint(level, {x: tile.bounds.x + tile.bounds.width, y: tile.bounds.y + tile.bounds.height});
//iterate over the tiles overlapped by this one
let x, y;
for(x = topLeft.x; x<= bottomRight.x; x++){
for(y = topLeft.y; y <= bottomRight.y; y++){
let mesh = tileContainer.userData._tileMatrix[x][y];
loadBestImage(mesh);
}
}
instance.renderFrame();
}
function setItemOrder(event, instance){
instance._imageContainer.children.forEach(child=>{
child.position.z = DEPTH_MULTIPLIER * instance._viewer.world.getIndexOfItem(child._tiledImage);
});
instance.renderFrame();
}
function updateMeshIfNeeded(tiledImage){
let tileContainer = tiledImage._three.userData._tileContainer;
let levelsInterval = tiledImage._getLevelsInterval();
let level = levelsInterval.highestLevel;
if(tileContainer.userData._tiledImageLevel === level){
//we are already drawing the highest-resolution tiles, just return
return;
}
console.log('new level', level);
tileContainer.userData._tiledImageLevel = level;
//we need to update the grid.
//clear the old matrix
tileContainer.userData._tileMatrix = [];
//remove existing tiles
tileContainer.children.forEach(cleanupObject);
tileContainer.clear();
//create new set of tiles and add to the tileContainer
let gridInfo = tiledImage.getGridDefinition(level);
let col, row;
for(col = 0; col < gridInfo.numColumns; col += 1){
tileContainer.userData._tileMatrix[col] = [];
for(row = 0; row < gridInfo.numRows; row += 1){
let colInfo = gridInfo.columnInfo[col];
let rowInfo = gridInfo.rowInfo[row];
let left = colInfo.x;
let top = rowInfo.y;
let x = left + colInfo.width / 2;
let y = top + rowInfo.height / 2;
let z = 0;
let tileGeometry = new THREE.PlaneGeometry(colInfo.width, rowInfo.height);
let mesh = new THREE.Mesh(tileGeometry);
mesh.position.set(x, y, z);
mesh._tileInfo = {
row: row,
col: col,
level: level,
...rowInfo,
...colInfo,
tiledImage: tiledImage,
center: new OpenSeadragon.Point(x, y),
uvMapOriginal: [],
// uvMap
}
let i;
let uvAttribute = tileGeometry.attributes.uv;
for(i =0 ; i<uvAttribute.count; ++i){
let x = uvAttribute.getX(i);
let y = uvAttribute.getY(i);
mesh._tileInfo.uvMapOriginal[i] = [x, y];
}
tileContainer.add(mesh);
tileContainer.userData._tileMatrix[col][row] = mesh;
//get (current) best image data for the tile
loadBestImage(mesh);
}
}
}
function loadBestImage(mesh){
let tileInfo = mesh._tileInfo;
let tiledImage = tileInfo.tiledImage;
let tileSource = tiledImage.source;
let tilesMatrix = tiledImage.tilesMatrix;
//if we have our own texture, use it
// let tile = tilesMatrix[tileInfo.level][tileInfo.col][tileInfo.row];
let tile = hasMaterial(tilesMatrix, tileInfo.level, tileInfo.col, tileInfo.row);
if(tile){
addMaterialToMesh(mesh, tile, tiledImage);
} else {
//start at next highest level and work downward
let queryLevel = tileInfo.level - 1;
while(queryLevel >= 0){
let tileIndex = tileSource.getTileAtPoint(queryLevel, tileInfo.center);
let tile = hasMaterial(tilesMatrix, queryLevel, tileIndex.x, tileIndex.y);
if(tile){
addMaterialToMesh(mesh, tile, tiledImage);
break;
}
queryLevel--;
}
}
}
function hasMaterial(tileMatrix, level, x, y){
let tile = tileMatrix[level] && tileMatrix[level][x] && tileMatrix[level][x][y];
return tile && tile._three ? tile : null;
}
function addMaterialToMesh(mesh, tile, tiledImage){
let regionInfo = mesh._tileInfo;
let material = tile._three;
let materialBounds = material.userData._tileBounds;
//update transparent and opacity properties to reflect current state
let opacity = tiledImage.opacity;
let transparent = opacity < 1 || material.userData.hasTransparency;
material.transparent = transparent;
material.opacity = opacity;
mesh.material = material;
let uvMap = mesh._tileInfo.uvMapOriginal;
let uvAttribute = mesh.geometry.attributes.uv;
// iterate over UV map for each vertex and calculate position within material/texture
let xNew, yNew;
let regionLeft = regionInfo.x;
let regionTop = regionInfo.y;
let regionRight = regionLeft + regionInfo.width;
let regionBottom = regionTop + regionInfo.height;
//what is needed to calculate the right uv index for each vertex?
// 1) position of the vertex in normalized coordinates
// 2) position of the entire texture area (not just non-overlapped area) in normalized coordinates
uvMap.forEach(([x,y],i)=>{
// x, y describe which corner of the original texture to use
if(x==0){
xNew = (regionLeft - materialBounds.x) / materialBounds.width;
} else {
xNew = (regionRight - materialBounds.x) / materialBounds.width;
}
if(y == 0){
yNew = (regionTop - materialBounds.y) / materialBounds.height;
} else {
yNew = (regionBottom - materialBounds.y) / materialBounds.height;
}
uvAttribute.setXY(i, xNew, yNew);
});
uvAttribute.needsUpdate = true;
}
function cleanupObject(object){
if(object.children && object.children.forEach){
object.children.forEach(cleanupObject);
}
if(object.dispose){
object.dispose();
}
if(object.geometry){
object.geometry.dispose();
}
}

View File

@ -16,77 +16,156 @@
</script>
<script type="module" src="./webgl.js"></script>
<style type="text/css">
.openseadragon1 {
width: 600px;
height: 400px;
.content{
max-width:960px;
margin: 0 auto;
}
#three-canvas{
position:fixed;
top:5px;
right:5px;
.mirrored{
display:grid;
grid-template-columns:50% 50%;
gap: 2em;
}
.viewer-container {
/* width: 600px;
height: 400px; */
aspect-ratio: 4 / 3;
border: thin gray solid;
position:relative;
}
.example-code{
background-color:tan;
border: thin black solid;
padding:10px;
display:inline-block;
}
.description pre{
display:inline-block;
background-color:gainsboro;
padding:0;
margin:0;
}
.image-options{
display: grid;
grid-template-columns: 2em repeat(7, 10em);
grid-template-columns: 2em 9em 1fr;
padding:3px;
border: thin gray solid;
}
.option-grid{
display: grid;
grid-template-columns: 7em 7em 10em 10em 10em;
/* grid-template-columns: repeat(5, auto); */
}
.image-options input[type=number]{
width: 5em;
}
.image-options select{
width: 5em;
}
</style>
</head>
<body>
<div>
Simple demo page to show three.js based rendering.
</div>
<div id="contentDiv" class="openseadragon1"></div>
<div class="content">
<h2>Use a WebGL drawer implementation (using three.js) instead of the default context2d drawer</h2>
<div class="mirrored">
<div>
<div class="description">
The context2d drawing operations in core OpenSeadragon have been
consolidated from the <pre>TiledImage</pre> and <pre>Tile</pre> classes into the
<pre>Drawer</pre> class, which inherits from a new <pre>DrawerBase</pre> base class.
The <pre>TiledImage</pre> and <pre>Tile</pre> classes now handle the logic of managing
tile positioning and image data, with cleaner separation from details of the rendering
process. <pre>DrawerBase</pre> defines a public API that core OpenSeadragon code uses to
interact with the drawer implementation. To use a custom drawer/render, define
a new class that inherits from <pre>DrawerBase</pre> and implements the public API.
The constructor of this class can be passed in during construction of the viewer using the
new <pre>customDrawer</pre> option.
</div>
<pre class="example-code">
import { ThreeJSDrawer } from './threejsdrawer.js';
<div id="image-picker">
<div class="image-options">
<span class="ui-icon ui-icon-arrowthick-2-n-s"></span>
<label><input type="checkbox" checked data-image="rainbow" class="toggle"> Rainbow Grid</label>
<label>X: <input type="number" value="0" data-image="rainbow" data-field="x"> </label>
<label>Y: <input type="number" value="0" data-image="rainbow" data-field="y"> </label>
<label>Width: <input type="number" value="1" data-image="rainbow" data-field="width" min="0"> </label>
<label>Degrees: <input type="number" value="0" data-image="rainbow" data-field="degrees"> </label>
<label>Opacity: <input type="number" value="1" data-image="rainbow" data-field="opacity" min="0" max="1" step="0.2"> </label>
<label>Flipped: <input type="checkbox" data-image="rainbow" data-field="flipped"></label>
let viewer = OpenSeadragon({
...
customDrawer: ThreeJSDrawer,
...
});
</pre>
</div>
<div id="three-viewer" class="viewer-container"></div>
</div>
<div class="image-options">
<span class="ui-icon ui-icon-arrowthick-2-n-s"></span>
<label><input type="checkbox" data-image="leaves" class="toggle"> Leaves</label>
<label>X: <input type="number" value="0.5" data-image="leaves" data-field="x"> </label>
<label>Y: <input type="number" value="0.5" data-image="leaves" data-field="y"> </label>
<label>Width: <input type="number" value="1.5" data-image="leaves" data-field="width" min="0"> </label>
<label>Degrees: <input type="number" value="0" data-image="leaves" data-field="degrees"> </label>
<label>Opacity: <input type="number" value="0.8" data-image="leaves" data-field="opacity" min="0" max="1" step="0.2"> </label>
<label>Flipped: <input type="checkbox" data-image="leaves" data-field="flipped"></label>
<h2>Compare behavior of <strong>Context2d</strong> and <strong>WebGL</strong> (via three.js) drawers</h2>
<div class="mirrored">
<div>
<h3>Use default OpenSeadragon viewer to pan/zoom</h3>
<div id="contentDiv" class="viewer-container"></div>
</div>
<div>
<h3>WebGL drawer linked using event listeners </h3>
<div id="three-canvas-container" class="viewer-container"></div>
</div>
</div>
<div class="image-options">
<span class="ui-icon ui-icon-arrowthick-2-n-s"></span>
<label><input type="checkbox" data-image="bblue" class="toggle"> BBlue PNG</label>
<label>X: <input type="number" value="0" data-image="bblue" data-field="x"> </label>
<label>Y: <input type="number" value="0" data-image="bblue" data-field="y"> </label>
<label>Width: <input type="number" value="0.5" data-image="bblue" data-field="width" min="0"> </label>
<label>Degrees: <input type="number" value="0" data-image="bblue" data-field="degrees"> </label>
<label>Opacity: <input type="number" value="1" data-image="bblue" data-field="opacity" min="0" max="1" step="0.2"> </label>
<label>Flipped: <input type="checkbox" data-image="bblue" data-field="flipped"></label>
<div id="image-picker">
<h3>Image options (drag and drop to re-order images)</h3>
<div class="image-options">
<span class="ui-icon ui-icon-arrowthick-2-n-s"></span>
<label><input type="checkbox" checked data-image="rainbow" class="toggle"> Rainbow Grid</label>
<div class="option-grid">
<label>X: <input type="number" value="0" data-image="rainbow" data-field="x"> </label>
<label>Y: <input type="number" value="0" data-image="rainbow" data-field="y"> </label>
<label>Width: <input type="number" value="1" data-image="rainbow" data-field="width" min="0"> </label>
<label>Degrees: <input type="number" value="0" data-image="rainbow" data-field="degrees"> </label>
<label>Opacity: <input type="number" value="1" data-image="rainbow" data-field="opacity" min="0" max="1" step="0.2"> </label>
<label>Flipped: <input type="checkbox" data-image="rainbow" data-field="flipped"></label>
<label>Cropped: <input type="checkbox" data-image="rainbow" data-field="cropped"></label>
<label>Debug: <input type="checkbox" data-image="rainbow" data-field="debug"></label>
<label>Composite: <select data-image="rainbow" data-field="composite"></select></label>
<label>Wrapping: <select data-image="rainbow" data-field="wrapping"></select></label>
</div>
</div>
<div class="image-options">
<span class="ui-icon ui-icon-arrowthick-2-n-s"></span>
<label><input type="checkbox" data-image="leaves" class="toggle"> Leaves</label>
<div class="option-grid">
<label>X: <input type="number" value="0" data-image="leaves" data-field="x"> </label>
<label>Y: <input type="number" value="0" data-image="leaves" data-field="y"> </label>
<label>Width: <input type="number" value="1" data-image="leaves" data-field="width" min="0"> </label>
<label>Degrees: <input type="number" value="0" data-image="leaves" data-field="degrees"> </label>
<label>Opacity: <input type="number" value="1" data-image="leaves" data-field="opacity" min="0" max="1" step="0.2"> </label>
<label>Flipped: <input type="checkbox" data-image="leaves" data-field="flipped"></label>
<label>Cropped: <input type="checkbox" data-image="leaves" data-field="cropped"></label>
<label>Debug: <input type="checkbox" data-image="leaves" data-field="debug"></label>
<label>Composite: <select data-image="leaves" data-field="composite" ></select></label>
<label>Wrapping: <select data-image="leaves" data-field="wrapping"></select></label>
</div>
</div>
<div class="image-options">
<span class="ui-icon ui-icon-arrowthick-2-n-s"></span>
<label><input type="checkbox" data-image="bblue" class="toggle"> BBlue PNG</label>
<div class="option-grid">
<label>X: <input type="number" value="0" data-image="bblue" data-field="x"> </label>
<label>Y: <input type="number" value="0" data-image="bblue" data-field="y"> </label>
<label>Width: <input type="number" value="1" data-image="bblue" data-field="width" min="0"> </label>
<label>Degrees: <input type="number" value="0" data-image="bblue" data-field="degrees"> </label>
<label>Opacity: <input type="number" value="1" data-image="bblue" data-field="opacity" min="0" max="1" step="0.2"> </label>
<label>Flipped: <input type="checkbox" data-image="bblue" data-field="flipped"></label>
<label>Cropped: <input type="checkbox" data-image="bblue" data-field="cropped"></label>
<label>Debug: <input type="checkbox" data-image="bblue" data-field="debug"></label>
<label>Composite: <select data-image="bblue" data-field="composite"></select></label>
<label>Wrapping: <select data-image="bblue" data-field="wrapping"></select></label>
</div>
</div>
</div>
<!-- <div class="image-options">
<span class="ui-icon ui-icon-arrowthick-2-n-s"></span>
<label><input type="checkbox" data-image="duomo" class="toggle"> Duomo</label>
<label>X: <input type="number" value="0" data-image="duomo" data-field="x"> </label>
<label>Y: <input type="number" value="0" data-image="duomo" data-field="y"> </label>
<label>Width: <input type="number" value="0.5" data-image="duomo" data-field="width"> </label>
<label>Degrees: <input type="number" value="0" data-image="duomo" data-field="degrees"> </label>
<label>Opacity: <input type="number" value="1" data-image="duomo" data-field="opacity"> </label>
<label>Flipped: <input type="checkbox" data-image="duomo" data-field="flipped"></label>
</div> -->
</div>
<canvas id="three-canvas"></canvas>
</body>
</html>

View File

@ -1,8 +1,8 @@
//imports
import { ThreeJSRenderer } from './webgl-renderer.js';
import { ThreeJSDrawer } from './threejsdrawer.js';
//globals
const canvas = document.querySelector('#three-canvas');
// const canvas = document.querySelector('#three-canvas');
const sources = {
"rainbow":"../data/testpattern.dzi",
"leaves":"../data/iiif_2_0_sizes/info.json",
@ -10,37 +10,38 @@ const sources = {
type:'image',
url: "../data/BBlue.png",
},
// "duomo":"https://openseadragon.github.io/example-images/highsmith/highsmith.dzi"
}
var viewer = window.viewer = OpenSeadragon({
// debugMode: true,
id: "contentDiv",
prefixUrl: "../../build/openseadragon/images/",
showNavigator:true,
minZoomImageRatio:0.001,
customRenderer: true, // set this to true to use a renderer plugin instead of the built-in drawer
useCanvas: {contextType: 'webgl2'} //set this to match the context type used by the plugin renderer
minZoomImageRatio:0.01,
});
//sync size
let threeRenderer = window.threeRenderer = new ThreeJSDrawer({viewer, viewport: viewer.viewport, element:viewer.element});
// let viewerCanvas = viewer.drawer.canvas;
// canvas.style.width = viewerCanvas.clientWidth+'px';
// canvas.style.height = viewerCanvas.clientHeight+'px';
// canvas.width = viewerCanvas.width;
// canvas.height = viewerCanvas.height;
var viewer2 = window.viewer2 = OpenSeadragon({
id: "three-viewer",
prefixUrl: "../../build/openseadragon/images/",
minZoomImageRatio:0.01,
customDrawer: ThreeJSDrawer,
tileSources: sources['leaves'],
imageSmoothingEnabled: false,
});
// //make the test canvas mirror all changes to the viewer canvas
// viewer.addHandler("resize", function(){
// canvas.style.width = viewerCanvas.clientWidth+'px';
// canvas.style.height = viewerCanvas.clientHeight+'px';
// })
let noCanvas;
//make the test canvas mirror all changes to the viewer canvas
let viewerCanvas = viewer.drawer.canvas;
let canvas = threeRenderer.canvas;
let canvasContainer = $('#three-canvas-container').append(canvas);
viewer.addHandler("resize", function(){
canvasContainer[0].style.width = viewerCanvas.clientWidth+'px';
canvasContainer[0].style.height = viewerCanvas.clientHeight+'px';
// canvas.width = viewerCanvas.width;
// canvas.height = viewerCanvas.height;
})
let threeRenderer = window.threeRenderer = new ThreeJSRenderer(viewer, noCanvas);
// viewer.addHandler("open", () => viewer.world.getItemAt(0).source.hasTransparency = function(){ return true; });
$('#three-viewer').resizable(true);
$('#contentDiv').resizable(true);
$('#image-picker').sortable({
update: function(event, ui){
@ -90,9 +91,82 @@ $('#image-picker input:not(.toggle)').on('change',function(){
tiledImage.setOpacity(Number(value));
} else if (field == 'flipped'){
tiledImage.setFlip($(this).prop('checked'));
} else if (field == 'cropped'){
if( $(this).prop('checked') ){
let croppingPolygons = [ [{x:200, y:200}, {x:800, y:200}, {x:500, y:800}] ];
tiledImage.setCroppingPolygons(croppingPolygons);
} else {
tiledImage.resetCroppingPolygons();
}
} else if (field == 'clipped'){
if( $(this).prop('checked') ){
let clipRect = new OpenSeadragon.Rect(2000, 0, 3000, 4000);
tiledImage.setClip(clipRect);
} else {
tiledImage.setClip(null);
}
}
else if (field == 'debug'){
if( $(this).prop('checked') ){
tiledImage.debugMode = true;
} else {
tiledImage.debugMode = false;
}
}
}
})
});
$('.image-options select[data-field=composite]').append(getCompositeOperationOptions()).on('change',function(){
let data = $(this).data();
let tiledImage = $(`#image-picker input.toggle[data-image=${data.image}]`).data('item');
if(tiledImage){
tiledImage.setCompositeOperation(this.value == 'null' ? null : this.value);
}
}).trigger('change');
$('.image-options select[data-field=wrapping]').append(getWrappingOptions()).on('change',function(){
let data = $(this).data();
let tiledImage = $(`#image-picker input.toggle[data-image=${data.image}]`).data('item');
if(tiledImage){
switch(this.value){
case "None": tiledImage.wrapHorizontal = tiledImage.wrapVertical = false; break;
case "Horizontal": tiledImage.wrapHorizontal = true; tiledImage.wrapVertical = false; break;
case "Vertical": tiledImage.wrapHorizontal = false; tiledImage.wrapVertical = true; break;
case "Both": tiledImage.wrapHorizontal = tiledImage.wrapVertical = true; break;
}
tiledImage.viewer.raiseEvent('opacity-change');//trigger a redraw for the webgl renderer. TODO: fix this hack.
}
}).trigger('change');
function getWrappingOptions(){
let opts = ['None', 'Horizontal', 'Vertical', 'Both'];
let elements = opts.map((opt, i)=>{
let el = $('<option>',{value:opt}).text(opt);
if(i===0){
el.attr('selected',true);
}
return el[0];
// $('.image-options select').append(el);
});
return $(elements);
}
function getCompositeOperationOptions(){
let opts = [null,'source-over','source-in','source-out','source-atop',
'destination-over','destination-in','destination-out','destination-atop',
'lighten','darken','copy','xor','multiply','screen','overlay','color-dodge',
'color-burn','hard-light','soft-light','difference','exclusion',
'hue','saturation','color','luminosity'];
let elements = opts.map((opt, i)=>{
let el = $('<option>',{value:opt}).text(opt);
if(i===0){
el.attr('selected',true);
}
return el[0];
// $('.image-options select').append(el);
});
return $(elements);
}
function addTileSource(image, checkbox){
let options = $(`#image-picker input[data-image=${image}][type=number]`).toArray().reduce((acc, input)=>{