mirror of
https://github.com/openseadragon/openseadragon.git
synced 2024-11-21 20:56:09 +03:00
Merge pull request #2472 from pearcetm/canvas-fallback
In webgl drawer, fall back to canvas drawer for tiled images with tainted data
This commit is contained in:
commit
01e70ab7d8
@ -139,6 +139,7 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
|
|||||||
this.canvas.height = 1;
|
this.canvas.height = 1;
|
||||||
this.sketchCanvas = null;
|
this.sketchCanvas = null;
|
||||||
this.sketchContext = null;
|
this.sketchContext = null;
|
||||||
|
this.container.removeChild(this.canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -280,6 +280,37 @@ OpenSeadragon.DrawerBase = class DrawerBase{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by implementations to fire the drawer-error event
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_raiseDrawerErrorEvent(tiledImage, errorMessage){
|
||||||
|
if(!this.viewer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raised when a tiled image is drawn to the canvas. Used internally for testing.
|
||||||
|
* The update-viewport event is preferred if you want to know when a frame has been drawn.
|
||||||
|
*
|
||||||
|
* @event drawer-error
|
||||||
|
* @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.DrawerBase} drawer - The drawer that raised the error.
|
||||||
|
* @property {String} error - A message describing the error.
|
||||||
|
* @property {?Object} userData - Arbitrary subscriber-defined object.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.viewer.raiseEvent( 'drawer-error', {
|
||||||
|
tiledImage: tiledImage,
|
||||||
|
drawer: this,
|
||||||
|
error: errorMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
}( OpenSeadragon ));
|
}( OpenSeadragon ));
|
||||||
|
@ -126,7 +126,7 @@ class HTMLDrawer extends OpenSeadragon.DrawerBase{
|
|||||||
* Destroy the drawer (unload current loaded tiles)
|
* Destroy the drawer (unload current loaded tiles)
|
||||||
*/
|
*/
|
||||||
destroy() {
|
destroy() {
|
||||||
this.canvas.innerHTML = "";
|
this.container.removeChild(this.canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -166,6 +166,7 @@ $.TiledImage = function( options ) {
|
|||||||
_lastDrawn: [], // array of tiles that were last fetched by the drawer
|
_lastDrawn: [], // array of tiles that were last fetched by the drawer
|
||||||
_isBlending: false, // Are any tiles still being blended?
|
_isBlending: false, // Are any tiles still being blended?
|
||||||
_wasBlending: false, // Were any tiles blending before the last draw?
|
_wasBlending: false, // Were any tiles blending before the last draw?
|
||||||
|
_isTainted: false, // Has a Tile been found with tainted data?
|
||||||
//configurable settings
|
//configurable settings
|
||||||
springStiffness: $.DEFAULT_SETTINGS.springStiffness,
|
springStiffness: $.DEFAULT_SETTINGS.springStiffness,
|
||||||
animationTime: $.DEFAULT_SETTINGS.animationTime,
|
animationTime: $.DEFAULT_SETTINGS.animationTime,
|
||||||
@ -326,6 +327,25 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||||||
return this._needsDraw;
|
return this._needsDraw;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the internal _isTainted flag for this TiledImage. Lazy loaded - not
|
||||||
|
* checked each time a Tile is loaded, but can be set if a consumer of the
|
||||||
|
* tiles (e.g. a Drawer) discovers a Tile to have tainted data so that further
|
||||||
|
* checks are not needed and alternative rendering strategies can be used.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
setTainted(isTainted){
|
||||||
|
this._isTainted = isTainted;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @returns {Boolean} whether the TiledImage has been marked as tainted
|
||||||
|
*/
|
||||||
|
isTainted(){
|
||||||
|
return this._isTainted;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroy the TiledImage (unload current loaded tiles).
|
* Destroy the TiledImage (unload current loaded tiles).
|
||||||
*/
|
*/
|
||||||
|
@ -455,35 +455,13 @@ $.Viewer = function( options ) {
|
|||||||
|
|
||||||
|
|
||||||
this.drawer = null;
|
this.drawer = null;
|
||||||
for (let i = 0; i < drawerCandidates.length; i++) {
|
for (const drawerCandidate of drawerCandidates){
|
||||||
|
let success = this.requestDrawer(drawerCandidate, {mainDrawer: true, redrawImmediately: false});
|
||||||
let drawerCandidate = drawerCandidates[i];
|
if(success){
|
||||||
let Drawer = null;
|
|
||||||
|
|
||||||
//if inherits from a drawer base, use it
|
|
||||||
if (drawerCandidate && drawerCandidate.prototype instanceof $.DrawerBase) {
|
|
||||||
Drawer = drawerCandidate;
|
|
||||||
drawerCandidate = 'custom';
|
|
||||||
} else if (typeof drawerCandidate === "string") {
|
|
||||||
Drawer = $.determineDrawer(drawerCandidate);
|
|
||||||
} else {
|
|
||||||
$.console.warn('Unsupported drawer! Drawer must be an existing string type, or a class that extends OpenSeadragon.DrawerBase.');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the drawer is supported, create it and break the loop
|
|
||||||
if (Drawer && Drawer.isSupported()) {
|
|
||||||
this.drawer = new Drawer({
|
|
||||||
viewer: this,
|
|
||||||
viewport: this.viewport,
|
|
||||||
element: this.canvas,
|
|
||||||
debugGridColor: this.debugGridColor,
|
|
||||||
options: this.drawerOptions[drawerCandidate],
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.drawer){
|
if (!this.drawer){
|
||||||
$.console.error('No drawer could be created!');
|
$.console.error('No drawer could be created!');
|
||||||
throw('Error with creating the selected drawer(s)');
|
throw('Error with creating the selected drawer(s)');
|
||||||
@ -950,6 +928,73 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
|
|||||||
this.removeAllHandlers();
|
this.removeAllHandlers();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request a drawer for this viewer, as a supported string or drawer constructor.
|
||||||
|
* @param {String | OpenSeadragon.DrawerBase} drawerCandidate The type of drawer to try to construct.
|
||||||
|
* @param { Object } options
|
||||||
|
* @param { Boolean } [options.mainDrawer] Whether to use this as the viewer's main drawer. Default = true.
|
||||||
|
* @param { Boolean } [options.redrawImmediately] Whether to immediately draw a new frame. Only used if options.mainDrawer = true. Default = true.
|
||||||
|
* @param { Object } [options.drawerOptions] Options for this drawer. Defaults to viewer.drawerOptions.
|
||||||
|
* for this viewer type. See {@link OpenSeadragon.Options}.
|
||||||
|
* @returns {Object | Boolean} The drawer that was created, or false if the requested drawer is not supported
|
||||||
|
*/
|
||||||
|
requestDrawer(drawerCandidate, options){
|
||||||
|
const defaultOpts = {
|
||||||
|
mainDrawer: true,
|
||||||
|
redrawImmediately: true,
|
||||||
|
drawerOptions: null
|
||||||
|
};
|
||||||
|
options = $.extend(true, defaultOpts, options);
|
||||||
|
const mainDrawer = options.mainDrawer;
|
||||||
|
const redrawImmediately = options.redrawImmediately;
|
||||||
|
const drawerOptions = options.drawerOptions;
|
||||||
|
|
||||||
|
const oldDrawer = this.drawer;
|
||||||
|
|
||||||
|
let Drawer = null;
|
||||||
|
|
||||||
|
//if the candidate inherits from a drawer base, use it
|
||||||
|
if (drawerCandidate && drawerCandidate.prototype instanceof $.DrawerBase) {
|
||||||
|
Drawer = drawerCandidate;
|
||||||
|
drawerCandidate = 'custom';
|
||||||
|
} else if (typeof drawerCandidate === "string") {
|
||||||
|
Drawer = $.determineDrawer(drawerCandidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!Drawer){
|
||||||
|
$.console.warn('Unsupported drawer! Drawer must be an existing string type, or a class that extends OpenSeadragon.DrawerBase.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the drawer is supported, create it and return true
|
||||||
|
if (Drawer && Drawer.isSupported()) {
|
||||||
|
|
||||||
|
// first destroy the previous drawer
|
||||||
|
if(oldDrawer && mainDrawer){
|
||||||
|
oldDrawer.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the new drawer
|
||||||
|
const newDrawer = new Drawer({
|
||||||
|
viewer: this,
|
||||||
|
viewport: this.viewport,
|
||||||
|
element: this.canvas,
|
||||||
|
debugGridColor: this.debugGridColor,
|
||||||
|
options: drawerOptions || this.drawerOptions[drawerCandidate],
|
||||||
|
});
|
||||||
|
|
||||||
|
if(mainDrawer){
|
||||||
|
this.drawer = newDrawer;
|
||||||
|
if(redrawImmediately){
|
||||||
|
this.forceRedraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newDrawer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @function
|
* @function
|
||||||
* @returns {Boolean}
|
* @returns {Boolean}
|
||||||
|
@ -90,10 +90,13 @@
|
|||||||
this._clippingCanvas = null;
|
this._clippingCanvas = null;
|
||||||
this._clippingContext = null;
|
this._clippingContext = null;
|
||||||
this._renderingCanvas = null;
|
this._renderingCanvas = null;
|
||||||
|
this._backupCanvasDrawer = null;
|
||||||
|
|
||||||
// Add listeners for events that require modifying the scene or camera
|
// Add listeners for events that require modifying the scene or camera
|
||||||
this.viewer.addHandler("tile-ready", ev => this._tileReadyHandler(ev));
|
this._boundToTileReady = ev => this._tileReadyHandler(ev);
|
||||||
this.viewer.addHandler("image-unloaded", ev => this._imageUnloadedHandler(ev));
|
this._boundToImageUnloaded = ev => this._imageUnloadedHandler(ev);
|
||||||
|
this.viewer.addHandler("tile-ready", this._boundToTileReady);
|
||||||
|
this.viewer.addHandler("image-unloaded", this._boundToImageUnloaded);
|
||||||
|
|
||||||
// Reject listening for the tile-drawing and tile-drawn events, which this drawer does not fire
|
// Reject listening for the tile-drawing and tile-drawn events, which this drawer does not fire
|
||||||
this.viewer.rejectEventHandler("tile-drawn", "The WebGLDrawer does not raise the tile-drawn event");
|
this.viewer.rejectEventHandler("tile-drawn", "The WebGLDrawer does not raise the tile-drawn event");
|
||||||
@ -154,9 +157,23 @@
|
|||||||
ext.loseContext();
|
ext.loseContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// unbind our event listeners from the viewer
|
||||||
|
this.viewer.removeHandler("tile-ready", this._boundToTileReady);
|
||||||
|
this.viewer.removeHandler("image-unloaded", this._boundToImageUnloaded);
|
||||||
|
|
||||||
// set our webgl context reference to null to enable garbage collection
|
// set our webgl context reference to null to enable garbage collection
|
||||||
this._gl = null;
|
this._gl = null;
|
||||||
|
|
||||||
|
if(this._backupCanvasDrawer){
|
||||||
|
this._backupCanvasDrawer.destroy();
|
||||||
|
this._backupCanvasDrawer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.container.removeChild(this.canvas);
|
||||||
|
if(this.viewer.drawer === this){
|
||||||
|
this.viewer.drawer = null;
|
||||||
|
}
|
||||||
|
|
||||||
// set our destroyed flag to true
|
// set our destroyed flag to true
|
||||||
this._destroyed = true;
|
this._destroyed = true;
|
||||||
}
|
}
|
||||||
@ -206,6 +223,21 @@
|
|||||||
return canvas;
|
return canvas;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the backup renderer (CanvasDrawer) to use if data cannot be used by webgl
|
||||||
|
* Lazy loaded
|
||||||
|
* @private
|
||||||
|
* @returns {CanvasDrawer}
|
||||||
|
*/
|
||||||
|
_getBackupCanvasDrawer(){
|
||||||
|
if(!this._backupCanvasDrawer){
|
||||||
|
this._backupCanvasDrawer = this.viewer.requestDrawer('canvas', {mainDrawer: false});
|
||||||
|
this._backupCanvasDrawer.canvas.style.setProperty('visibility', 'hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._backupCanvasDrawer;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {Array} tiledImages Array of TiledImage objects to draw
|
* @param {Array} tiledImages Array of TiledImage objects to draw
|
||||||
@ -237,172 +269,200 @@
|
|||||||
//iterate over tiled images and draw each one using a two-pass rendering pipeline if needed
|
//iterate over tiled images and draw each one using a two-pass rendering pipeline if needed
|
||||||
tiledImages.forEach( (tiledImage, tiledImageIndex) => {
|
tiledImages.forEach( (tiledImage, tiledImageIndex) => {
|
||||||
|
|
||||||
let tilesToDraw = tiledImage.getTilesToDraw();
|
if(tiledImage.isTainted()){
|
||||||
|
// first, draw any data left in the rendering buffer onto the output canvas
|
||||||
if ( tiledImage.placeholderFillStyle && tiledImage._hasOpaqueTile === false ) {
|
|
||||||
this._drawPlaceholder(tiledImage);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(tilesToDraw.length === 0 || tiledImage.getOpacity() === 0){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let firstTile = tilesToDraw[0];
|
|
||||||
|
|
||||||
let useContext2dPipeline = ( tiledImage.compositeOperation ||
|
|
||||||
this.viewer.compositeOperation ||
|
|
||||||
tiledImage._clip ||
|
|
||||||
tiledImage._croppingPolygons ||
|
|
||||||
tiledImage.debugMode
|
|
||||||
);
|
|
||||||
|
|
||||||
let useTwoPassRendering = useContext2dPipeline || (tiledImage.opacity < 1) || firstTile.hasTransparency;
|
|
||||||
|
|
||||||
// using the context2d pipeline requires a clean rendering (back) buffer to start
|
|
||||||
if(useContext2dPipeline){
|
|
||||||
// if the rendering buffer has image data currently, write it to the output canvas now and clear it
|
|
||||||
|
|
||||||
if(renderingBufferHasImageData){
|
if(renderingBufferHasImageData){
|
||||||
this._outputContext.drawImage(this._renderingCanvas, 0, 0);
|
this._outputContext.drawImage(this._renderingCanvas, 0, 0);
|
||||||
|
// clear the buffer
|
||||||
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||||
|
gl.clear(gl.COLOR_BUFFER_BIT); // clear the back buffer
|
||||||
|
renderingBufferHasImageData = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear the buffer
|
// next, use the backup canvas drawer to draw this tainted image
|
||||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
const canvasDrawer = this._getBackupCanvasDrawer();
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT); // clear the back buffer
|
canvasDrawer.draw([tiledImage]);
|
||||||
}
|
this._outputContext.drawImage(canvasDrawer.canvas, 0, 0);
|
||||||
|
|
||||||
// First rendering pass: compose tiles that make up this tiledImage
|
|
||||||
gl.useProgram(this._firstPass.shaderProgram);
|
|
||||||
|
|
||||||
// bind to the framebuffer for render-to-texture if using two-pass rendering, otherwise back buffer (null)
|
|
||||||
if(useTwoPassRendering){
|
|
||||||
gl.bindFramebuffer(gl.FRAMEBUFFER, this._glFrameBuffer);
|
|
||||||
// clear the buffer to draw a new image
|
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
||||||
} else {
|
} else {
|
||||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
let tilesToDraw = tiledImage.getTilesToDraw();
|
||||||
// no need to clear, just draw on top of the existing pixels
|
|
||||||
}
|
|
||||||
|
|
||||||
let overallMatrix = viewMatrix;
|
if ( tiledImage.placeholderFillStyle && tiledImage._hasOpaqueTile === false ) {
|
||||||
|
this._drawPlaceholder(tiledImage);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
let maxTextures = this._gl.getParameter(this._gl.MAX_TEXTURE_IMAGE_UNITS);
|
|
||||||
if(maxTextures <= 0){
|
|
||||||
// This can apparently happen on some systems if too many WebGL contexts have been created
|
|
||||||
// in which case maxTextures can be null, leading to out of bounds errors with the array.
|
|
||||||
// For example, when viewers were created and not destroyed in the test suite, this error
|
|
||||||
// occured in the TravisCI tests, though it did not happen when testing locally either in
|
|
||||||
// a browser or on the command line via grunt test.
|
|
||||||
|
|
||||||
throw(new Error(`WegGL error: bad value for gl parameter MAX_TEXTURE_IMAGE_UNITS (${maxTextures}). This could happen
|
|
||||||
if too many contexts have been created and not released, or there is another problem with the graphics card.`));
|
|
||||||
}
|
|
||||||
|
|
||||||
let texturePositionArray = new Float32Array(maxTextures * 12); // 6 vertices (2 triangles) x 2 coordinates per vertex
|
|
||||||
let textureDataArray = new Array(maxTextures);
|
|
||||||
let matrixArray = new Array(maxTextures);
|
|
||||||
let opacityArray = new Array(maxTextures);
|
|
||||||
|
|
||||||
// iterate over tiles and add data for each one to the buffers
|
|
||||||
for(let tileIndex = 0; tileIndex < tilesToDraw.length; tileIndex++){
|
|
||||||
let tile = tilesToDraw[tileIndex].tile;
|
|
||||||
let indexInDrawArray = tileIndex % maxTextures;
|
|
||||||
let numTilesToDraw = indexInDrawArray + 1;
|
|
||||||
let tileContext = tile.getCanvasContext();
|
|
||||||
|
|
||||||
let textureInfo = tileContext ? this._TextureMap.get(tileContext.canvas) : null;
|
|
||||||
if(textureInfo){
|
|
||||||
this._getTileData(tile, tiledImage, textureInfo, overallMatrix, indexInDrawArray, texturePositionArray, textureDataArray, matrixArray, opacityArray);
|
|
||||||
} else {
|
|
||||||
// console.log('No tile info', tile);
|
|
||||||
}
|
}
|
||||||
if( (numTilesToDraw === maxTextures) || (tileIndex === tilesToDraw.length - 1)){
|
|
||||||
// We've filled up the buffers: time to draw this set of tiles
|
|
||||||
|
|
||||||
// bind each tile's texture to the appropriate gl.TEXTURE#
|
if(tilesToDraw.length === 0 || tiledImage.getOpacity() === 0){
|
||||||
for(let i = 0; i <= numTilesToDraw; i++){
|
return;
|
||||||
gl.activeTexture(gl.TEXTURE0 + i);
|
}
|
||||||
gl.bindTexture(gl.TEXTURE_2D, textureDataArray[i]);
|
let firstTile = tilesToDraw[0];
|
||||||
|
|
||||||
|
let useContext2dPipeline = ( tiledImage.compositeOperation ||
|
||||||
|
this.viewer.compositeOperation ||
|
||||||
|
tiledImage._clip ||
|
||||||
|
tiledImage._croppingPolygons ||
|
||||||
|
tiledImage.debugMode
|
||||||
|
);
|
||||||
|
|
||||||
|
let useTwoPassRendering = useContext2dPipeline || (tiledImage.opacity < 1) || firstTile.hasTransparency;
|
||||||
|
|
||||||
|
// using the context2d pipeline requires a clean rendering (back) buffer to start
|
||||||
|
if(useContext2dPipeline){
|
||||||
|
// if the rendering buffer has image data currently, write it to the output canvas now and clear it
|
||||||
|
|
||||||
|
if(renderingBufferHasImageData){
|
||||||
|
this._outputContext.drawImage(this._renderingCanvas, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// set the buffer data for the texture coordinates to use for each tile
|
// clear the buffer
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._firstPass.bufferTexturePosition);
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, texturePositionArray, gl.DYNAMIC_DRAW);
|
gl.clear(gl.COLOR_BUFFER_BIT); // clear the back buffer
|
||||||
|
}
|
||||||
|
|
||||||
// set the transform matrix uniform for each tile
|
// First rendering pass: compose tiles that make up this tiledImage
|
||||||
matrixArray.forEach( (matrix, index) => {
|
gl.useProgram(this._firstPass.shaderProgram);
|
||||||
gl.uniformMatrix3fv(this._firstPass.uTransformMatrices[index], false, matrix);
|
|
||||||
});
|
|
||||||
// set the opacity uniform for each tile
|
|
||||||
gl.uniform1fv(this._firstPass.uOpacities, new Float32Array(opacityArray));
|
|
||||||
|
|
||||||
// bind vertex buffers and (re)set attributes before calling gl.drawArrays()
|
// bind to the framebuffer for render-to-texture if using two-pass rendering, otherwise back buffer (null)
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._firstPass.bufferOutputPosition);
|
if(useTwoPassRendering){
|
||||||
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this._glFrameBuffer);
|
||||||
|
// clear the buffer to draw a new image
|
||||||
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||||
|
} else {
|
||||||
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||||
|
// no need to clear, just draw on top of the existing pixels
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
let maxTextures = this._gl.getParameter(this._gl.MAX_TEXTURE_IMAGE_UNITS);
|
||||||
|
if(maxTextures <= 0){
|
||||||
|
// This can apparently happen on some systems if too many WebGL contexts have been created
|
||||||
|
// in which case maxTextures can be null, leading to out of bounds errors with the array.
|
||||||
|
// For example, when viewers were created and not destroyed in the test suite, this error
|
||||||
|
// occured in the TravisCI tests, though it did not happen when testing locally either in
|
||||||
|
// a browser or on the command line via grunt test.
|
||||||
|
|
||||||
|
throw(new Error(`WegGL error: bad value for gl parameter MAX_TEXTURE_IMAGE_UNITS (${maxTextures}). This could happen
|
||||||
|
if too many contexts have been created and not released, or there is another problem with the graphics card.`));
|
||||||
|
}
|
||||||
|
|
||||||
|
let texturePositionArray = new Float32Array(maxTextures * 12); // 6 vertices (2 triangles) x 2 coordinates per vertex
|
||||||
|
let textureDataArray = new Array(maxTextures);
|
||||||
|
let matrixArray = new Array(maxTextures);
|
||||||
|
let opacityArray = new Array(maxTextures);
|
||||||
|
|
||||||
|
// iterate over tiles and add data for each one to the buffers
|
||||||
|
for(let tileIndex = 0; tileIndex < tilesToDraw.length; tileIndex++){
|
||||||
|
let tile = tilesToDraw[tileIndex].tile;
|
||||||
|
let indexInDrawArray = tileIndex % maxTextures;
|
||||||
|
let numTilesToDraw = indexInDrawArray + 1;
|
||||||
|
let tileContext = tile.getCanvasContext();
|
||||||
|
|
||||||
|
let textureInfo = tileContext ? this._TextureMap.get(tileContext.canvas) : null;
|
||||||
|
if(!textureInfo){
|
||||||
|
// tile was not processed in the tile-ready event (this can happen
|
||||||
|
// if this drawer was created after the tile was downloaded)
|
||||||
|
this._tileReadyHandler({tile: tile, tiledImage: tiledImage});
|
||||||
|
|
||||||
|
// retry getting textureInfo
|
||||||
|
textureInfo = tileContext ? this._TextureMap.get(tileContext.canvas) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(textureInfo){
|
||||||
|
this._getTileData(tile, tiledImage, textureInfo, overallMatrix, indexInDrawArray, texturePositionArray, textureDataArray, matrixArray, opacityArray);
|
||||||
|
} else {
|
||||||
|
// console.log('No tile info', tile);
|
||||||
|
}
|
||||||
|
if( (numTilesToDraw === maxTextures) || (tileIndex === tilesToDraw.length - 1)){
|
||||||
|
// We've filled up the buffers: time to draw this set of tiles
|
||||||
|
|
||||||
|
// bind each tile's texture to the appropriate gl.TEXTURE#
|
||||||
|
for(let i = 0; i <= numTilesToDraw; i++){
|
||||||
|
gl.activeTexture(gl.TEXTURE0 + i);
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, textureDataArray[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the buffer data for the texture coordinates to use for each tile
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, this._firstPass.bufferTexturePosition);
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, texturePositionArray, gl.DYNAMIC_DRAW);
|
||||||
|
|
||||||
|
// set the transform matrix uniform for each tile
|
||||||
|
matrixArray.forEach( (matrix, index) => {
|
||||||
|
gl.uniformMatrix3fv(this._firstPass.uTransformMatrices[index], false, matrix);
|
||||||
|
});
|
||||||
|
// set the opacity uniform for each tile
|
||||||
|
gl.uniform1fv(this._firstPass.uOpacities, new Float32Array(opacityArray));
|
||||||
|
|
||||||
|
// bind vertex buffers and (re)set attributes before calling gl.drawArrays()
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, this._firstPass.bufferOutputPosition);
|
||||||
|
gl.vertexAttribPointer(this._firstPass.aOutputPosition, 2, gl.FLOAT, false, 0, 0);
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, this._firstPass.bufferTexturePosition);
|
||||||
|
gl.vertexAttribPointer(this._firstPass.aTexturePosition, 2, gl.FLOAT, false, 0, 0);
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, this._firstPass.bufferIndex);
|
||||||
|
gl.vertexAttribPointer(this._firstPass.aIndex, 1, gl.FLOAT, false, 0, 0);
|
||||||
|
|
||||||
|
// Draw! 6 vertices per tile (2 triangles per rectangle)
|
||||||
|
gl.drawArrays(gl.TRIANGLES, 0, 6 * numTilesToDraw );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(useTwoPassRendering){
|
||||||
|
// Second rendering pass: Render the tiled image from the framebuffer into the back buffer
|
||||||
|
gl.useProgram(this._secondPass.shaderProgram);
|
||||||
|
|
||||||
|
// set the rendering target to the back buffer (null)
|
||||||
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||||
|
|
||||||
|
// bind the rendered texture from the first pass to use during this second pass
|
||||||
|
gl.activeTexture(gl.TEXTURE0);
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, this._renderToTexture);
|
||||||
|
|
||||||
|
// set opacity to the value for the current tiledImage
|
||||||
|
this._gl.uniform1f(this._secondPass.uOpacityMultiplier, tiledImage.opacity);
|
||||||
|
|
||||||
|
// bind buffers and set attributes before calling gl.drawArrays
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, this._secondPass.bufferTexturePosition);
|
||||||
|
gl.vertexAttribPointer(this._secondPass.aTexturePosition, 2, gl.FLOAT, false, 0, 0);
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, this._secondPass.bufferOutputPosition);
|
||||||
gl.vertexAttribPointer(this._firstPass.aOutputPosition, 2, gl.FLOAT, false, 0, 0);
|
gl.vertexAttribPointer(this._firstPass.aOutputPosition, 2, gl.FLOAT, false, 0, 0);
|
||||||
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._firstPass.bufferTexturePosition);
|
// Draw the quad (two triangles)
|
||||||
gl.vertexAttribPointer(this._firstPass.aTexturePosition, 2, gl.FLOAT, false, 0, 0);
|
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||||
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._firstPass.bufferIndex);
|
}
|
||||||
gl.vertexAttribPointer(this._firstPass.aIndex, 1, gl.FLOAT, false, 0, 0);
|
|
||||||
|
|
||||||
// Draw! 6 vertices per tile (2 triangles per rectangle)
|
renderingBufferHasImageData = true;
|
||||||
gl.drawArrays(gl.TRIANGLES, 0, 6 * numTilesToDraw );
|
|
||||||
|
if(useContext2dPipeline){
|
||||||
|
// draw from the rendering canvas onto the output canvas, clipping/cropping if needed.
|
||||||
|
this._applyContext2dPipeline(tiledImage, tilesToDraw, tiledImageIndex);
|
||||||
|
renderingBufferHasImageData = false;
|
||||||
|
// clear the buffer
|
||||||
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||||
|
gl.clear(gl.COLOR_BUFFER_BIT); // clear the back buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// after drawing the first TiledImage, fire the tiled-image-drawn event (for testing)
|
||||||
|
if(tiledImageIndex === 0){
|
||||||
|
this._raiseTiledImageDrawnEvent(tiledImage, tilesToDraw.map(info=>info.tile));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(useTwoPassRendering){
|
|
||||||
// Second rendering pass: Render the tiled image from the framebuffer into the back buffer
|
|
||||||
gl.useProgram(this._secondPass.shaderProgram);
|
|
||||||
|
|
||||||
// set the rendering target to the back buffer (null)
|
|
||||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
||||||
|
|
||||||
// bind the rendered texture from the first pass to use during this second pass
|
|
||||||
gl.activeTexture(gl.TEXTURE0);
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, this._renderToTexture);
|
|
||||||
|
|
||||||
// set opacity to the value for the current tiledImage
|
|
||||||
this._gl.uniform1f(this._secondPass.uOpacityMultiplier, tiledImage.opacity);
|
|
||||||
|
|
||||||
// bind buffers and set attributes before calling gl.drawArrays
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._secondPass.bufferTexturePosition);
|
|
||||||
gl.vertexAttribPointer(this._secondPass.aTexturePosition, 2, gl.FLOAT, false, 0, 0);
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._secondPass.bufferOutputPosition);
|
|
||||||
gl.vertexAttribPointer(this._firstPass.aOutputPosition, 2, gl.FLOAT, false, 0, 0);
|
|
||||||
|
|
||||||
// Draw the quad (two triangles)
|
|
||||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
renderingBufferHasImageData = true;
|
|
||||||
|
|
||||||
if(useContext2dPipeline){
|
|
||||||
// draw from the rendering canvas onto the output canvas, clipping/cropping if needed.
|
|
||||||
this._applyContext2dPipeline(tiledImage, tilesToDraw, tiledImageIndex);
|
|
||||||
renderingBufferHasImageData = false;
|
|
||||||
// clear the buffer
|
|
||||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT); // clear the back buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
// after drawing the first TiledImage, fire the tiled-image-drawn event (for testing)
|
|
||||||
if(tiledImageIndex === 0){
|
|
||||||
this._raiseTiledImageDrawnEvent(tiledImage, tilesToDraw.map(info=>info.tile));
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -805,8 +865,28 @@
|
|||||||
_tileReadyHandler(event){
|
_tileReadyHandler(event){
|
||||||
let tile = event.tile;
|
let tile = event.tile;
|
||||||
let tiledImage = event.tiledImage;
|
let tiledImage = event.tiledImage;
|
||||||
|
|
||||||
|
// If a tiledImage is already known to be tainted, don't try to upload any
|
||||||
|
// textures to webgl, because they won't be used even if it succeeds
|
||||||
|
if(tiledImage.isTainted()){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let tileContext = tile.getCanvasContext();
|
let tileContext = tile.getCanvasContext();
|
||||||
let canvas = tileContext.canvas;
|
let canvas = tileContext && tileContext.canvas;
|
||||||
|
// if the tile doesn't provide a canvas, or is tainted by cross-origin
|
||||||
|
// data, marked the TiledImage as tainted so the canvas drawer can be
|
||||||
|
// used instead, and return immediately - tainted data cannot be uploaded to webgl
|
||||||
|
if(!canvas || $.isCanvasTainted(canvas)){
|
||||||
|
const wasTainted = tiledImage.isTainted();
|
||||||
|
if(!wasTainted){
|
||||||
|
tiledImage.setTainted(true);
|
||||||
|
$.console.warn('WebGL cannot be used to draw this TiledImage because it has tainted data. Does crossOriginPolicy need to be set?');
|
||||||
|
this._raiseDrawerErrorEvent(tiledImage, 'Tainted data cannot be used by the WebGLDrawer. Falling back to CanvasDrawer for this TiledImage.');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let textureInfo = this._TextureMap.get(canvas);
|
let textureInfo = this._TextureMap.get(canvas);
|
||||||
|
|
||||||
// if this is a new image for us, create a texture
|
// if this is a new image for us, create a texture
|
||||||
|
Loading…
Reference in New Issue
Block a user