mirror of
https://github.com/openseadragon/openseadragon.git
synced 2024-11-24 22:26:10 +03:00
finished many implementation details and demo
This commit is contained in:
parent
f9ab63944b
commit
5328761877
@ -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",
|
||||
|
707
src/drawer.js
707
src/drawer.js
@ -46,6 +46,8 @@
|
||||
*/
|
||||
$.Drawer = function(options) {
|
||||
|
||||
$.DrawerBase.call(this, options);
|
||||
|
||||
$.console.assert( options.viewer, "[Drawer] options.viewer is required" );
|
||||
|
||||
//backward compatibility for positional args while preferring more
|
||||
@ -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
344
src/drawerbase.js
Normal 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 <canvas> element if the browser supports them, otherwise a <div> 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 ));
|
165
src/tile.js
165
src/tile.js
@ -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
|
||||
|
@ -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 ));
|
||||
|
@ -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;
|
||||
|
@ -419,12 +419,29 @@ $.Viewer = function( options ) {
|
||||
});
|
||||
|
||||
// Create the drawer
|
||||
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" );
|
||||
|
@ -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
796
test/demo/threejsdrawer.js
Normal 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();
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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 class="content">
|
||||
<h2>Use a WebGL drawer implementation (using three.js) instead of the default context2d drawer</h2>
|
||||
<div class="mirrored">
|
||||
<div>
|
||||
Simple demo page to show three.js based rendering.
|
||||
<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>
|
||||
<div id="contentDiv" class="openseadragon1"></div>
|
||||
<pre class="example-code">
|
||||
import { ThreeJSDrawer } from './threejsdrawer.js';
|
||||
|
||||
let viewer = OpenSeadragon({
|
||||
...
|
||||
customDrawer: ThreeJSDrawer,
|
||||
...
|
||||
});
|
||||
</pre>
|
||||
</div>
|
||||
<div id="three-viewer" class="viewer-container"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<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 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>
|
||||
<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>
|
||||
<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="0.8" data-image="leaves" data-field="opacity" min="0" max="1" step="0.2"> </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="0.5" data-image="bblue" data-field="width" min="0"> </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 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>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
@ -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';
|
||||
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
|
||||
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;
|
||||
})
|
||||
|
||||
// //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;
|
||||
|
||||
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)=>{
|
||||
|
Loading…
Reference in New Issue
Block a user