2023-06-29 23:41:45 +03:00
// THIS CODE OVERWRITES THE ORIGINAL VERSION FOR FASTER TESTING
// i.e. it doesn't need to be re-built with grunt after every save.
/ *
* OpenSeadragon - WebGLDrawer
*
* 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 ( $ ) {
2023-07-31 22:48:29 +03:00
/ * *
* @ 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 .
* /
$ . WebGLDrawer = class WebGLDrawer extends OpenSeadragon . DrawerBase {
constructor ( options ) {
super ( options ) ;
this . destroyed = false ;
// private members
this . _TextureMap = new Map ( ) ;
this . _TileMap = new Map ( ) ;
2023-06-29 23:41:45 +03:00
this . _gl = null ;
2023-07-31 22:48:29 +03:00
this . _firstPass = null ;
this . _secondPass = null ;
this . _glFrameBuffer = null ;
this . _renderToTexture = null ;
this . _glFramebufferToCanvasTransform = null ;
this . _outputCanvas = null ;
this . _outputContext = null ;
this . _clippingCanvas = null ;
this . _clippingContext = null ;
this . _renderingCanvas = null ;
2023-06-29 23:41:45 +03:00
2023-07-31 22:48:29 +03:00
// Add listeners for events that require modifying the scene or camera
this . viewer . addHandler ( "tile-ready" , ev => this . _tileReadyHandler ( ev ) ) ;
this . viewer . addHandler ( "image-unloaded" , ev => this . _imageUnloadedHandler ( ev ) ) ;
2023-06-29 23:41:45 +03:00
2023-07-31 22:48:29 +03:00
// this.viewer is set by parent constructor
// this.canvas is set by parent constructor, created and appended to the viewer container element
this . _setupCanvases ( ) ;
2023-06-29 23:41:45 +03:00
2023-07-31 22:48:29 +03:00
this . _setupRenderer ( ) ;
2023-06-29 23:41:45 +03:00
2023-07-31 22:48:29 +03:00
this . context = this . _outputContext ; // API required by tests
2023-06-29 23:41:45 +03:00
}
2023-07-31 22:48:29 +03:00
// Public API required by all Drawer implementations
/ * *
* Clean up the renderer , removing all resources
* /
destroy ( ) {
if ( this . destroyed ) {
return ;
}
// clear all resources used by the renderer, geometries, textures etc
let gl = this . _gl ;
// adapted from https://stackoverflow.com/a/23606581/1214731
var numTextureUnits = gl . getParameter ( gl . MAX _TEXTURE _IMAGE _UNITS ) ;
for ( let unit = 0 ; unit < numTextureUnits ; ++ unit ) {
gl . activeTexture ( gl . TEXTURE0 + unit ) ;
gl . bindTexture ( gl . TEXTURE _2D , null ) ;
gl . bindTexture ( gl . TEXTURE _CUBE _MAP , null ) ;
}
gl . bindBuffer ( gl . ARRAY _BUFFER , null ) ;
gl . bindBuffer ( gl . ELEMENT _ARRAY _BUFFER , null ) ;
gl . bindRenderbuffer ( gl . RENDERBUFFER , null ) ;
gl . bindFramebuffer ( gl . FRAMEBUFFER , null ) ;
let canvases = Array . from ( this . _TextureMap . keys ( ) ) ;
canvases . forEach ( canvas => {
this . _cleanupImageData ( canvas ) ; // deletes texture, removes from _TextureMap
} ) ;
// Delete all our created resources
gl . deleteBuffer ( this . _secondPass . bufferOutputPosition ) ;
gl . deleteFramebuffer ( this . _glFrameBuffer ) ;
// TODO: if/when render buffers or frame buffers are used, release them:
// gl.deleteRenderbuffer(someRenderbuffer);
// gl.deleteFramebuffer(someFramebuffer);
// make canvases 1 x 1 px and delete references
this . _renderingCanvas . width = this . _renderingCanvas . height = 1 ;
this . _clippingCanvas . width = this . _clippingCanvas . height = 1 ;
this . _outputCanvas . width = this . _outputCanvas . height = 1 ;
this . _renderingCanvas = null ;
this . _clippingCanvas = this . _clippingContext = null ;
this . _outputCanvas = this . _outputContext = null ;
let ext = gl . getExtension ( 'WEBGL_lose_context' ) ;
if ( ext ) {
ext . loseContext ( ) ;
}
// set our webgl context reference to null to enable garbage collection
this . _gl = null ;
// set our destroyed flag to true
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 ( ) {
let canvasElement = document . createElement ( 'canvas' ) ;
let webglContext = $ . isFunction ( canvasElement . getContext ) &&
canvasElement . getContext ( 'webgl' ) ;
let ext = webglContext . getExtension ( 'WEBGL_lose_context' ) ;
if ( ext ) {
ext . loseContext ( ) ;
}
return ! ! ( webglContext ) ;
}
getType ( ) {
return 'webgl' ;
}
/ * *
* create the HTML element ( canvas in this case ) that the image will be drawn into
* @ returns { Element } the canvas to draw into
* /
createDrawingElement ( ) {
let canvas = $ . makeNeutralElement ( "canvas" ) ;
let viewportSize = this . _calculateCanvasSize ( ) ;
canvas . width = viewportSize . x ;
canvas . height = viewportSize . y ;
return canvas ;
}
/ * *
*
* @ param { Array } tiledImages Array of TiledImage objects to draw
* /
draw ( tiledImages ) {
let gl = this . _gl ;
let viewport = {
bounds : this . viewport . getBoundsNoRotate ( true ) ,
center : this . viewport . getCenter ( true ) ,
rotation : this . viewport . getRotation ( true ) * Math . PI / 180
} ;
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 * flipMultiplier , - 2 / viewport . bounds . height ) ;
let rotMatrix = $ . Mat3 . makeRotation ( - viewport . rotation ) ;
let viewMatrix = scaleMatrix . multiply ( rotMatrix ) . multiply ( posMatrix ) ;
gl . bindFramebuffer ( gl . FRAMEBUFFER , null ) ;
gl . clear ( gl . COLOR _BUFFER _BIT ) ; // clear the back buffer
// clear the output canvas
this . _outputContext . clearRect ( 0 , 0 , this . _outputCanvas . width , this . _outputCanvas . height ) ;
let renderingBufferHasImageData = false ;
//iterate over tiled images and draw each one using a two-pass rendering pipeline if needed
tiledImages . forEach ( ( tiledImage , tiledImageIndex ) => {
let useContext2dPipeline = ( tiledImage . compositeOperation ||
this . viewer . compositeOperation ||
tiledImage . _clip ||
tiledImage . _croppingPolygons ||
tiledImage . debugMode
) ;
let useTwoPassRendering = useContext2dPipeline || ( tiledImage . opacity < 1 ) ; // TODO: check hasTransparency in addition to opacity
let tilesToDraw = tiledImage . getTilesToDraw ( ) ;
if ( tilesToDraw . length === 0 ) {
return ;
}
// 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 ) ;
}
// clear the buffer
gl . bindFramebuffer ( gl . FRAMEBUFFER , null ) ;
gl . clear ( gl . COLOR _BUFFER _BIT ) ; // clear the back buffer
}
// 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 {
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 ) ;
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#
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 ) ;
}
}
// gl.flush(); // is this necessary?
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 ) ;
// TODO: is this the mechanism we want to use here?
// iterate over any filters - filters can use this._renderToTexture to get rendered data if desired
let filters = this . filters || [ ] ;
for ( let fi = 0 ; fi < filters . length ; fi ++ ) {
let filter = this . filters [ fi ] ;
if ( filter . apply ) {
filter . apply ( gl ) ; // filter.apply should write data on top of the backbuffer (bound above)
}
}
}
renderingBufferHasImageData = true ;
// gl.flush(); //make sure drawing to the output buffer of the rendering canvas is complete. Is this necessary?
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
}
// 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 ) ,
} ) ;
}
} ) ;
if ( renderingBufferHasImageData ) {
this . _outputContext . drawImage ( this . _renderingCanvas , 0 , 0 ) ;
}
}
// Public API required by all Drawer implementations
/ * *
* Set the context2d imageSmoothingEnabled parameter
* @ param { Boolean } enabled
* /
setImageSmoothingEnabled ( enabled ) {
this . _clippingContext . imageSmoothingEnabled = enabled ;
this . _outputContext . imageSmoothingEnabled = enabled ;
}
/ * *
* Draw a rect onto the output canvas for debugging purposes
* @ param { OpenSeadragon . Rect } rect
* /
drawDebuggingRect ( rect ) {
let context = this . _outputContext ;
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 ( ) ;
}
// private
_getTextureDataFromTile ( tile ) {
return tile . getCanvasContext ( ) . canvas ;
}
/ * *
* Draw data from the rendering canvas onto the output canvas , with clipping ,
* cropping and / or debug info as requested .
* @ private
* @ param { OpenSeadragon . TiledImage } tiledImage - the tiledImage to draw
* @ param { Array } tilesToDraw - array of objects containing tiles that were drawn
* /
_applyContext2dPipeline ( tiledImage , tilesToDraw , tiledImageIndex ) {
// composite onto the output canvas, clipping if necessary
this . _outputContext . save ( ) ;
// set composite operation; ignore for first image drawn
this . _outputContext . globalCompositeOperation = tiledImageIndex === 0 ? null : tiledImage . compositeOperation || this . viewer . compositeOperation ;
if ( tiledImage . _croppingPolygons || tiledImage . _clip ) {
this . _renderToClippingCanvas ( tiledImage ) ;
this . _outputContext . drawImage ( this . _clippingCanvas , 0 , 0 ) ;
} else {
this . _outputContext . drawImage ( this . _renderingCanvas , 0 , 0 ) ;
}
this . _outputContext . restore ( ) ;
if ( tiledImage . debugMode ) {
let colorIndex = this . viewer . world . getIndexOfItem ( tiledImage ) % this . debugGridColor . length ;
let strokeStyle = this . debugGridColor [ colorIndex ] ;
let fillStyle = this . debugGridColor [ colorIndex ] ;
this . _drawDebugInfo ( tilesToDraw , tiledImage , strokeStyle , fillStyle ) ;
}
}
// private
_getTileData ( tile , tiledImage , textureInfo , viewMatrix , index , texturePositionArray , textureDataArray , matrixArray , opacityArray ) {
let texture = textureInfo . texture ;
let textureQuad = textureInfo . position ;
// set the position of this texture
texturePositionArray . set ( textureQuad , index * 12 ) ;
// 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 ) ;
opacityArray [ index ] = tile . opacity ; // * tiledImage.opacity;
textureDataArray [ index ] = texture ;
matrixArray [ index ] = overallMatrix . values ;
if ( this . continuousTileRefresh ) {
// Upload the image into the texture
// TODO: test if this works appropriately
let tileContext = tile . getCanvasContext ( ) ;
this . _raiseTileDrawingEvent ( tiledImage , this . _outputContext , tile , tileContext ) ;
this . _uploadImageData ( tileContext , tile , tiledImage ) ;
}
}
_setupRenderer ( ) {
let gl = this . _gl ;
if ( ! gl ) {
$ . console . error ( '_setupCanvases must be called before _setupRenderer' ) ;
}
this . _unitQuad = this . _makeQuadVertexBuffer ( 0 , 1 , 0 , 1 ) ; // used a few places; create once and store the result
this . _makeFirstPassShaderProgram ( ) ;
this . _makeSecondPassShaderProgram ( ) ;
// set up the texture to render to in the first pass, and which will be used for rendering the second pass
this . _renderToTexture = gl . createTexture ( ) ;
gl . activeTexture ( gl . TEXTURE0 ) ;
gl . bindTexture ( gl . TEXTURE _2D , this . _renderToTexture ) ;
gl . texImage2D ( gl . TEXTURE _2D , 0 , gl . RGBA , this . _renderingCanvas . width , this . _renderingCanvas . height , 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 . enable ( gl . BLEND ) ;
gl . blendFunc ( gl . ONE , gl . ONE _MINUS _SRC _ALPHA ) ;
}
_makeFirstPassShaderProgram ( ) {
let numTextures = this . _glNumTextures = this . _gl . getParameter ( this . _gl . MAX _TEXTURE _IMAGE _UNITS ) ;
let makeMatrixUniforms = ( ) => {
return [ ... Array ( numTextures ) . keys ( ) ] . map ( index => ` uniform mat3 u_matrix_ ${ index } ; ` ) . join ( '\n' ) ;
} ;
let makeConditionals = ( ) => {
return [ ... Array ( numTextures ) . keys ( ) ] . map ( index => ` ${ index > 0 ? 'else ' : '' } if(int(a_index) == ${ index } ) { transform_matrix = u_matrix_ ${ index } ; } ` ) . join ( '\n' ) ;
} ;
const vertexShaderProgram = `
attribute vec2 a _output _position ;
attribute vec2 a _texture _position ;
attribute float a _index ;
$ { makeMatrixUniforms ( ) } // create a uniform mat3 for each potential tile to draw
varying vec2 v _texture _position ;
varying float v _image _index ;
void main ( ) {
mat3 transform _matrix ; // value will be set by the if/elses in makeConditional()
$ { makeConditionals ( ) }
gl _Position = vec4 ( transform _matrix * vec3 ( a _output _position , 1 ) , 1 ) ;
v _texture _position = a _texture _position ;
v _image _index = a _index ;
}
` ;
const fragmentShaderProgram = `
precision mediump float ;
// our textures
uniform sampler2D u _images [ $ { numTextures } ] ;
// our opacities
uniform float u _opacities [ $ { numTextures } ] ;
// the varyings passed in from the vertex shader.
varying vec2 v _texture _position ;
varying float v _image _index ;
void main ( ) {
// can't index directly with a variable, need to use a loop iterator hack
for ( int i = 0 ; i < $ { numTextures } ; ++ i ) {
if ( i == int ( v _image _index ) ) {
gl _FragColor = texture2D ( u _images [ i ] , v _texture _position ) * u _opacities [ i ] ;
}
}
}
` ;
let gl = this . _gl ;
let program = this . constructor . initShaderProgram ( gl , vertexShaderProgram , fragmentShaderProgram ) ;
gl . useProgram ( program ) ;
// get locations of attributes and uniforms, and create buffers for each attribute
this . _firstPass = {
shaderProgram : program ,
aOutputPosition : gl . getAttribLocation ( program , 'a_output_position' ) ,
aTexturePosition : gl . getAttribLocation ( program , 'a_texture_position' ) ,
aIndex : gl . getAttribLocation ( program , 'a_index' ) ,
uTransformMatrices : [ ... Array ( this . _glNumTextures ) . keys ( ) ] . map ( i => gl . getUniformLocation ( program , ` u_matrix_ ${ i } ` ) ) ,
uImages : gl . getUniformLocation ( program , 'u_images' ) ,
uOpacities : gl . getUniformLocation ( program , 'u_opacities' ) ,
bufferOutputPosition : gl . createBuffer ( ) ,
bufferTexturePosition : gl . createBuffer ( ) ,
bufferIndex : gl . createBuffer ( ) ,
} ;
gl . uniform1iv ( this . _firstPass . uImages , [ ... Array ( numTextures ) . keys ( ) ] ) ;
// provide coordinates for the rectangle in output space, i.e. a unit quad for each one.
let outputQuads = new Float32Array ( numTextures * 12 ) ;
for ( let i = 0 ; i < numTextures ; ++ i ) {
outputQuads . set ( Float32Array . from ( this . _unitQuad ) , i * 12 ) ;
}
gl . bindBuffer ( gl . ARRAY _BUFFER , this . _firstPass . bufferOutputPosition ) ;
gl . bufferData ( gl . ARRAY _BUFFER , outputQuads , gl . STATIC _DRAW ) ; // bind data statically here, since it's unchanging
gl . enableVertexAttribArray ( this . _firstPass . aOutputPosition ) ;
// provide texture coordinates for the rectangle in image (texture) space. Data will be set later.
gl . bindBuffer ( gl . ARRAY _BUFFER , this . _firstPass . bufferTexturePosition ) ;
gl . enableVertexAttribArray ( this . _firstPass . aTexturePosition ) ;
// for each vertex, provide an index into the array of textures/matrices to use for the correct tile
gl . bindBuffer ( gl . ARRAY _BUFFER , this . _firstPass . bufferIndex ) ;
let indices = [ ... Array ( this . _glNumTextures ) . keys ( ) ] . map ( i => Array ( 6 ) . fill ( i ) ) . flat ( ) ; // repeat each index 6 times, for the 6 vertices per tile (2 triangles)
gl . bufferData ( gl . ARRAY _BUFFER , new Float32Array ( indices ) , gl . STATIC _DRAW ) ; // bind data statically here, since it's unchanging
gl . enableVertexAttribArray ( this . _firstPass . aIndex ) ;
}
_makeSecondPassShaderProgram ( ) {
const vertexShaderProgram = `
attribute vec2 a _output _position ;
attribute vec2 a _texture _position ;
uniform mat3 u _matrix ;
varying vec2 v _texture _position ;
void main ( ) {
gl _Position = vec4 ( u _matrix * vec3 ( a _output _position , 1 ) , 1 ) ;
v _texture _position = a _texture _position ;
}
` ;
const fragmentShaderProgram = `
precision mediump float ;
// our texture
uniform sampler2D u _image ;
// the texCoords passed in from the vertex shader.
varying vec2 v _texture _position ;
// the opacity multiplier for the image
uniform float u _opacity _multiplier ;
void main ( ) {
gl _FragColor = texture2D ( u _image , v _texture _position ) ;
gl _FragColor *= u _opacity _multiplier ;
}
` ;
let gl = this . _gl ;
let program = this . constructor . initShaderProgram ( gl , vertexShaderProgram , fragmentShaderProgram ) ;
gl . useProgram ( program ) ;
// get locations of attributes and uniforms, and create buffers for each attribute
this . _secondPass = {
shaderProgram : program ,
aOutputPosition : gl . getAttribLocation ( program , 'a_output_position' ) ,
aTexturePosition : gl . getAttribLocation ( program , 'a_texture_position' ) ,
uMatrix : gl . getUniformLocation ( program , 'u_matrix' ) ,
uImage : gl . getUniformLocation ( program , 'u_image' ) ,
uOpacityMultiplier : gl . getUniformLocation ( program , 'u_opacity_multiplier' ) ,
bufferOutputPosition : gl . createBuffer ( ) ,
bufferTexturePosition : gl . createBuffer ( ) ,
} ;
// provide coordinates for the rectangle in output space, i.e. a unit quad for each one.
gl . bindBuffer ( gl . ARRAY _BUFFER , this . _secondPass . bufferOutputPosition ) ;
gl . bufferData ( gl . ARRAY _BUFFER , this . _unitQuad , gl . STATIC _DRAW ) ; // bind data statically here since it's unchanging
gl . enableVertexAttribArray ( this . _secondPass . aOutputPosition ) ;
// provide texture coordinates for the rectangle in image (texture) space.
gl . bindBuffer ( gl . ARRAY _BUFFER , this . _secondPass . bufferTexturePosition ) ;
gl . bufferData ( gl . ARRAY _BUFFER , this . _unitQuad , gl . DYNAMIC _DRAW ) ; // bind data statically here since it's unchanging
gl . enableVertexAttribArray ( this . _secondPass . aTexturePosition ) ;
// set the matrix that transforms the framebuffer to clip space
let matrix = $ . Mat3 . makeScaling ( 2 , 2 ) . multiply ( $ . Mat3 . makeTranslation ( - 0.5 , - 0.5 ) ) ;
gl . uniformMatrix3fv ( this . _secondPass . uMatrix , false , matrix . values ) ;
}
_resizeRenderer ( ) {
let gl = this . _gl ;
let w = this . _renderingCanvas . width ;
let h = this . _renderingCanvas . height ;
gl . viewport ( 0 , 0 , w , h ) ;
//release the old texture
gl . deleteTexture ( this . _renderToTexture ) ;
//create a new texture and set it up
this . _renderToTexture = gl . createTexture ( ) ;
gl . activeTexture ( gl . TEXTURE0 ) ;
gl . bindTexture ( gl . TEXTURE _2D , this . _renderToTexture ) ;
gl . texImage2D ( gl . TEXTURE _2D , 0 , gl . RGBA , w , h , 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 ) ;
//bind the frame buffer to the new texture
gl . bindFramebuffer ( gl . FRAMEBUFFER , this . _glFrameBuffer ) ;
gl . framebufferTexture2D ( gl . FRAMEBUFFER , gl . COLOR _ATTACHMENT0 , gl . TEXTURE _2D , this . _renderToTexture , 0 ) ;
}
_setupCanvases ( ) {
let _this = this ;
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 ;
this . _gl = this . _renderingCanvas . getContext ( 'webgl' ) ;
//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 . _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 ;
// important - update the size of the rendering viewport!
_this . _resizeRenderer ( ) ;
} ) ;
}
_makeQuadVertexBuffer ( left , right , top , bottom ) {
return new Float32Array ( [
left , bottom ,
right , bottom ,
left , top ,
left , top ,
right , bottom ,
right , top ] ) ;
}
_tileReadyHandler ( event ) {
let tile = event . tile ;
let tiledImage = event . tiledImage ;
let tileContext = tile . getCanvasContext ( ) ;
let canvas = tileContext . canvas ;
let textureInfo = this . _TextureMap . get ( canvas ) ;
// if this is a new image for us, create a texture
if ( ! textureInfo ) {
let gl = this . _gl ;
// create a gl Texture for this tile and bind the canvas with the image data
let texture = gl . createTexture ( ) ;
let position ;
let overlap = tiledImage . source . tileOverlap ;
if ( overlap > 0 ) {
// calculate the normalized position of the rect to actually draw
// discarding overlap.
let overlapFraction = this . _calculateOverlapFraction ( tile , tiledImage ) ;
let left = tile . x === 0 ? 0 : overlapFraction . x ;
let top = tile . y === 0 ? 0 : overlapFraction . y ;
let right = tile . isRightMost ? 1 : 1 - overlapFraction . x ;
let bottom = tile . isBottomMost ? 1 : 1 - overlapFraction . y ;
position = this . _makeQuadVertexBuffer ( left , right , top , bottom ) ;
} else {
// no overlap: this texture can use the unit quad as it's position data
position = this . _unitQuad ;
}
let textureInfo = {
texture : texture ,
position : position ,
} ;
// add it to our _TextureMap
this . _TextureMap . set ( canvas , textureInfo ) ;
gl . activeTexture ( gl . TEXTURE0 ) ;
gl . bindTexture ( gl . TEXTURE _2D , texture ) ;
// Set the parameters so we can render any size image.
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 ) ;
gl . texParameteri ( gl . TEXTURE _2D , gl . TEXTURE _MIN _FILTER , gl . LINEAR ) ;
gl . texParameteri ( gl . TEXTURE _2D , gl . TEXTURE _MAG _FILTER , gl . LINEAR ) ;
// Upload the image into the texture.
this . _uploadImageData ( tileContext ) ;
}
}
_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
} ;
}
_uploadImageData ( tileContext ) {
let gl = this . _gl ;
let canvas = tileContext . canvas ;
try {
if ( ! canvas ) {
throw ( 'Tile context does not have a canvas' , tileContext ) ;
}
// This depends on gl.TEXTURE_2D being bound to the texture
// associated with this canvas before calling this function
gl . texImage2D ( gl . TEXTURE _2D , 0 , gl . RGBA , gl . RGBA , gl . UNSIGNED _BYTE , canvas ) ;
} catch ( e ) {
$ . console . error ( 'Error uploading image data to WebGL' , e ) ;
}
}
_imageUnloadedHandler ( event ) {
let canvas = event . context2D . canvas ;
this . _cleanupImageData ( canvas ) ;
}
_cleanupImageData ( tileCanvas ) {
let textureInfo = this . _TextureMap . get ( tileCanvas ) ;
//remove from the map
this . _TextureMap . delete ( tileCanvas ) ;
//release the texture from the GPU
if ( textureInfo ) {
this . _gl . deleteTexture ( textureInfo . texture ) ;
}
// release the position buffer from the GPU
// TODO: do this!
}
// private
// necessary for clip testing to pass (test uses spyOnce(drawer._setClip))
_setClip ( rect ) {
this . _clippingContext . beginPath ( ) ;
this . _clippingContext . rect ( rect . x , rect . y , rect . width , rect . height ) ;
this . _clippingContext . clip ( ) ;
}
_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 . _setClip ( rect ) ;
}
if ( item . _croppingPolygons ) {
let polygons = item . _croppingPolygons . map ( function ( polygon ) {
return polygon . map ( function ( coord ) {
let point = item . imageToViewportCoordinates ( coord . x , coord . y , true )
. rotate ( _this . viewer . viewport . getRotation ( true ) , _this . viewer . viewport . getCenter ( 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 ( $ . pixelDensityRatio ) :
new $ . 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
* @ inner
* 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 ( point ) {
var vpPoint = this . viewport . pixelFromPointNoRotate ( point , true ) ;
return new $ . Point (
vpPoint . x * $ . pixelDensityRatio ,
vpPoint . y * $ . pixelDensityRatio
) ;
}
// private
_drawDebugInfo ( tilesToDraw , tiledImage , stroke , fill ) {
for ( var i = tilesToDraw . length - 1 ; i >= 0 ; i -- ) {
var tile = tilesToDraw [ i ] . tile ;
try {
this . _drawDebugInfoOnTile ( tile , tilesToDraw . length , i , tiledImage , stroke , fill ) ;
} catch ( e ) {
$ . console . error ( e ) ;
}
}
}
// private
_drawDebugInfoOnTile ( tile , count , i , tiledImage , stroke , fill ) {
var context = this . _outputContext ;
context . save ( ) ;
context . lineWidth = 2 * $ . pixelDensityRatio ;
context . font = 'small-caps bold ' + ( 13 * $ . pixelDensityRatio ) + 'px arial' ;
context . strokeStyle = stroke ;
context . fillStyle = fill ;
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
_restoreRotationChanges ( ) {
var context = this . _outputContext ;
context . restore ( ) ;
}
// modified from https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Adding_2D_content_to_a_WebGL_context
static initShaderProgram ( gl , vsSource , fsSource ) {
const vertexShader = loadShader ( gl , gl . VERTEX _SHADER , vsSource ) ;
const fragmentShader = loadShader ( gl , gl . FRAGMENT _SHADER , fsSource ) ;
// Create the shader program
const shaderProgram = gl . createProgram ( ) ;
gl . attachShader ( shaderProgram , vertexShader ) ;
gl . attachShader ( shaderProgram , fragmentShader ) ;
gl . linkProgram ( shaderProgram ) ;
// If creating the shader program failed, alert
if ( ! gl . getProgramParameter ( shaderProgram , gl . LINK _STATUS ) ) {
alert (
` Unable to initialize the shader program: ${ gl . getProgramInfoLog (
shaderProgram
) } `
) ;
return null ;
}
return shaderProgram ;
function loadShader ( gl , type , source ) {
const shader = gl . createShader ( type ) ;
// Send the source to the shader object
gl . shaderSource ( shader , source ) ;
// Compile the shader program
gl . compileShader ( shader ) ;
// See if it compiled successfully
if ( ! gl . getShaderParameter ( shader , gl . COMPILE _STATUS ) ) {
alert (
` An error occurred compiling the shaders: ${ gl . getShaderInfoLog ( shader ) } `
) ;
gl . deleteShader ( shader ) ;
return null ;
}
return shader ;
}
}
} ;
} ( OpenSeadragon ) ) ;
2023-06-29 23:41:45 +03:00