mirror of
https://github.com/openseadragon/openseadragon.git
synced 2025-04-02 21:36:11 +03:00
320 lines
13 KiB
JavaScript
320 lines
13 KiB
JavaScript
|
|
/*
|
|
* OpenSeadragon - WebGLDrawer
|
|
*
|
|
* 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 WebGLDrawer
|
|
* @memberof OpenSeadragon
|
|
* @classdesc Default implementation of WebGLDrawer 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.
|
|
* @param {Number} [options.debugGridColor] - See debugGridColor in {@link OpenSeadragon.Options} for details.
|
|
*/
|
|
|
|
$.WebGL = class WebGL extends OpenSeadragon.DrawerBase {
|
|
constructor(options){
|
|
super(options);
|
|
|
|
this.destroyed = false;
|
|
// Add listeners for events that require modifying the scene or camera
|
|
this.viewer.addHandler("tile-ready", this._tileReadyHandler.bind(this));
|
|
this.viewer.addHandler("image-unloaded", this.renderer.freeData.bind(this.renderer));
|
|
}
|
|
|
|
// Public API required by all Drawer implementations
|
|
/**
|
|
* Clean up the renderer, removing all resources
|
|
*/
|
|
destroy(){
|
|
if(this.destroyed){
|
|
return;
|
|
}
|
|
//todo
|
|
this.destroyed = true;
|
|
}
|
|
|
|
// Public API required by all Drawer implementations
|
|
/**
|
|
*
|
|
* @returns true if the drawer supports rotation
|
|
*/
|
|
canRotate(){
|
|
return true;
|
|
}
|
|
|
|
// Public API required by all Drawer implementations
|
|
|
|
/**
|
|
* @returns {Boolean} returns true if canvas and webgl are supported
|
|
*/
|
|
static isSupported(){
|
|
return true; //todo
|
|
}
|
|
|
|
getType() {
|
|
return 'universal_webgl';
|
|
}
|
|
|
|
/**
|
|
* create the HTML element (canvas in this case) that the image will be drawn into
|
|
* @returns {Element} the canvas to draw into
|
|
*/
|
|
createDrawingElement(){
|
|
|
|
const engine = new $.WebGLModule($.extend(this.options, {
|
|
uniqueId: "openseadragon",
|
|
"2.0": {
|
|
canvasOptions: {
|
|
stencil: true
|
|
}
|
|
}
|
|
}));
|
|
|
|
engine.addRenderingSpecifications({
|
|
shaders: {
|
|
renderShader: {
|
|
type: "identity",
|
|
dataReferences: [0],
|
|
}
|
|
}
|
|
});
|
|
|
|
engine.prepare();
|
|
|
|
const size = this._calculateCanvasSize();
|
|
engine.init(size.x, size.y);
|
|
this.viewer.addHandler("resize", this._resizeRenderer.bind(this));
|
|
this.renderer = engine;
|
|
this.renderer.setDataBlendingEnabled(true);
|
|
|
|
const gl = this.renderer.gl;
|
|
// this._renderToTexture = gl.createTexture();
|
|
// gl.activeTexture(gl.TEXTURE0);
|
|
// gl.bindTexture(gl.TEXTURE_2D, this._renderToTexture);
|
|
// gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, size.x, size.y, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
//
|
|
// // set up the framebuffer for render-to-texture
|
|
// this._glFrameBuffer = gl.createFramebuffer();
|
|
// gl.bindFramebuffer(gl.FRAMEBUFFER, this._glFrameBuffer);
|
|
// gl.framebufferTexture2D(
|
|
// gl.FRAMEBUFFER,
|
|
// gl.COLOR_ATTACHMENT0, // attach texture as COLOR_ATTACHMENT0
|
|
// gl.TEXTURE_2D, // attach a 2D texture
|
|
// this._renderToTexture, // the texture to attach
|
|
// 0
|
|
// );
|
|
// gl.bindFramebuffer(gl.FRAMEBUFFER, this._glFrameBuffer);
|
|
// gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this._renderToTexture, 0);
|
|
|
|
gl.enable(gl.STENCIL_TEST);
|
|
gl.stencilMask(0xff);
|
|
gl.stencilFunc(gl.GREATER, 1, 0xff);
|
|
gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
|
|
return engine.canvas;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Array} tiledImages Array of TiledImage objects to draw
|
|
*/
|
|
draw(tiledImages){
|
|
let viewport = {
|
|
bounds: this.viewport.getBoundsNoRotate(true),
|
|
center: this.viewport.getCenter(true),
|
|
rotation: this.viewport.getRotation(true) * Math.PI / 180,
|
|
zoom: this.viewport.getZoom(true)
|
|
};
|
|
|
|
|
|
// let flipMultiplier = this.viewport.flipped ? -1 : 1;
|
|
// calculate view matrix for viewer
|
|
let posMatrix = $.Mat3.makeTranslation(-viewport.center.x, -viewport.center.y);
|
|
let scaleMatrix = $.Mat3.makeScaling(2 / viewport.bounds.width, -2 / viewport.bounds.height);
|
|
let rotMatrix = $.Mat3.makeRotation(-viewport.rotation);
|
|
let viewMatrix = scaleMatrix.multiply(rotMatrix).multiply(posMatrix);
|
|
|
|
const gl = this.renderer.gl;
|
|
// gl.bindFramebuffer(gl.FRAMEBUFFER, this._glFrameBuffer);
|
|
// clear the buffer to draw a new image
|
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
|
|
//iterate over tiled images and draw each one using a two-pass rendering pipeline if needed
|
|
for (const tiledImage of tiledImages) {
|
|
let tilesToDraw = tiledImage.getTilesToDraw();
|
|
|
|
if (tilesToDraw.length === 0) {
|
|
continue;
|
|
}
|
|
|
|
gl.clear(gl.STENCIL_BUFFER_BIT);
|
|
|
|
let overallMatrix = viewMatrix;
|
|
let imageRotation = tiledImage.getRotation(true);
|
|
// if needed, handle the tiledImage being rotated
|
|
if( imageRotation % 360 !== 0){
|
|
let imageRotationMatrix = $.Mat3.makeRotation(-imageRotation * Math.PI / 180);
|
|
let imageCenter = tiledImage.getBoundsNoRotate(true).getCenter();
|
|
let t1 = $.Mat3.makeTranslation(imageCenter.x, imageCenter.y);
|
|
let t2 = $.Mat3.makeTranslation(-imageCenter.x, -imageCenter.y);
|
|
|
|
// update the view matrix to account for this image's rotation
|
|
let localMatrix = t1.multiply(imageRotationMatrix).multiply(t2);
|
|
overallMatrix = viewMatrix.multiply(localMatrix);
|
|
}
|
|
|
|
|
|
//todo better access to the rendering context
|
|
const shader = this.renderer.specification(0).shaders.renderShader._renderContext;
|
|
// iterate over tiles and add data for each one to the buffers
|
|
for (let tileIndex = tilesToDraw.length - 1; tileIndex >= 0; tileIndex--){
|
|
const tile = tilesToDraw[tileIndex].tile;
|
|
|
|
const matrix = this._getTileMatrix(tile, tiledImage, overallMatrix);
|
|
shader.opacity.set(tile.opacity * tiledImage.opacity);
|
|
|
|
//todo pixelSize value (not yet memoized)
|
|
this.renderer.processData(tile.cacheKey, {
|
|
transform: matrix,
|
|
zoom: viewport.zoom,
|
|
pixelSize: 0
|
|
});
|
|
}
|
|
|
|
// Fire tiled-image-drawn event.
|
|
// TODO: the image data may not be on the output canvas yet!!
|
|
if( this.viewer ){
|
|
/**
|
|
* Raised when a tiled image is drawn to the canvas. Only valid
|
|
* for webgl drawer.
|
|
*
|
|
* @event tiled-image-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 {Array} tiles - An array of Tile objects that were drawn.
|
|
* @property {?Object} userData - Arbitrary subscriber-defined object.
|
|
*/
|
|
this.viewer.raiseEvent( 'tiled-image-drawn', {
|
|
tiledImage: tiledImage,
|
|
tiles: tilesToDraw.map(info => info.tile),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the context2d imageSmoothingEnabled parameter
|
|
* @param {Boolean} enabled
|
|
*/
|
|
setImageSmoothingEnabled(enabled){
|
|
//todo
|
|
// this._clippingContext.imageSmoothingEnabled = enabled;
|
|
// this._outputContext.imageSmoothingEnabled = enabled;
|
|
}
|
|
|
|
// private
|
|
_getTileMatrix(tile, tiledImage, viewMatrix){
|
|
// compute offsets that account for tile overlap; needed for calculating the transform matrix appropriately
|
|
let overlapFraction = this._calculateOverlapFraction(tile, tiledImage);
|
|
let xOffset = tile.positionedBounds.width * overlapFraction.x;
|
|
let yOffset = tile.positionedBounds.height * overlapFraction.y;
|
|
|
|
// x, y, w, h in viewport coords
|
|
let x = tile.positionedBounds.x + (tile.x === 0 ? 0 : xOffset);
|
|
let y = tile.positionedBounds.y + (tile.y === 0 ? 0 : yOffset);
|
|
let right = tile.positionedBounds.x + tile.positionedBounds.width - (tile.isRightMost ? 0 : xOffset);
|
|
let bottom = tile.positionedBounds.y + tile.positionedBounds.height - (tile.isBottomMost ? 0 : yOffset);
|
|
let w = right - x;
|
|
let h = bottom - y;
|
|
|
|
let matrix = new $.Mat3([
|
|
w, 0, 0,
|
|
0, h, 0,
|
|
x, y, 1,
|
|
]);
|
|
|
|
if(tile.flipped){
|
|
// flip the tile around the center of the unit quad
|
|
let t1 = $.Mat3.makeTranslation(0.5, 0);
|
|
let t2 = $.Mat3.makeTranslation(-0.5, 0);
|
|
|
|
// update the view matrix to account for this image's rotation
|
|
let localMatrix = t1.multiply($.Mat3.makeScaling(-1, 1)).multiply(t2);
|
|
matrix = matrix.multiply(localMatrix);
|
|
}
|
|
|
|
let overallMatrix = viewMatrix.multiply(matrix);
|
|
return overallMatrix.values;
|
|
}
|
|
|
|
_resizeRenderer(){
|
|
const size = this._calculateCanvasSize();
|
|
this.renderer.setDimensions(0, 0, size.x, size.y);
|
|
}
|
|
|
|
_imageUnloadedHandler(event){
|
|
this.renderer.freeData(event.tile.cacheKey);
|
|
}
|
|
|
|
_tileReadyHandler(event){
|
|
//todo tile overlap
|
|
let tile = event.tile;
|
|
//todo fix cache system and then this line
|
|
//access by default raw tile data, and only access canvas if not cache set
|
|
let data = tile.cacheImageRecord ? tile.cacheImageRecord.getData() : tile.getCanvasContext().canvas;
|
|
this.renderer.loadData(tile.cacheKey, data, tile.sourceBounds.width, tile.sourceBounds.height);
|
|
}
|
|
|
|
_calculateOverlapFraction(tile, tiledImage){
|
|
let overlap = tiledImage.source.tileOverlap;
|
|
let nativeWidth = tile.sourceBounds.width; // in pixels
|
|
let nativeHeight = tile.sourceBounds.height; // in pixels
|
|
let overlapWidth = (tile.x === 0 ? 0 : overlap) + (tile.isRightMost ? 0 : overlap); // in pixels
|
|
let overlapHeight = (tile.y === 0 ? 0 : overlap) + (tile.isBottomMost ? 0 : overlap); // in pixels
|
|
let widthOverlapFraction = overlap / (nativeWidth + overlapWidth); // as a fraction of image including overlap
|
|
let heightOverlapFraction = overlap / (nativeHeight + overlapHeight); // as a fraction of image including overlap
|
|
return {
|
|
x: widthOverlapFraction,
|
|
y: heightOverlapFraction
|
|
};
|
|
}
|
|
};
|
|
}( OpenSeadragon ));
|