openseadragon/src/webgl/drawer.js

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 ));