draw(tiledImages){
            let gl = this._gl;
            const bounds = this.viewport.getBoundsNoRotateWithMargins(true);
            let view = {
                bounds: bounds,
                center: new OpenSeadragon.Point(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2),
                rotation: this.viewport.getRotation(true) * Math.PI / 180
            };

            let flipMultiplier = this.viewport.flipped ? -1 : 1;
            // calculate view matrix for viewer
            let posMatrix = $.Mat3.makeTranslation(-view.center.x, -view.center.y);
            let scaleMatrix = $.Mat3.makeScaling(2 / view.bounds.width * flipMultiplier, -2 / view.bounds.height);
            let rotMatrix = $.Mat3.makeRotation(-view.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) => {

                if(tiledImage.isTainted()){
                    // first, draw any data left in the rendering buffer onto the output canvas
                    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
                        renderingBufferHasImageData = false;
                    }

                    // next, use the backup canvas drawer to draw this tainted image
                    const canvasDrawer = this._getBackupCanvasDrawer();
                    canvasDrawer.draw([tiledImage]);
                    this._outputContext.drawImage(canvasDrawer.canvas, 0, 0);

                } else {
                    let tilesToDraw = tiledImage.getTilesToDraw();

                    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.tile.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);
                        }

                        // 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);
                    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
                        // occurred 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;
                        const textureInfo = this.getDataToDraw(tile);

                        if (textureInfo && textureInfo.texture) {
                            this._getTileData(tile, tiledImage, textureInfo, overallMatrix, indexInDrawArray, texturePositionArray, textureDataArray, matrixArray, opacityArray);
                        }
                        // else {
                        //   If the texture info is not available, we cannot draw this tile. This is either because
                        //   the tile data is still being processed, or the data was not correct - in that case,
                        //   internalCacheCreate(..) already logged an error.
                        // }

                        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._secondPass.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));
                    }
                }

            });

            if(renderingBufferHasImageData){
                this._outputContext.drawImage(this._renderingCanvas, 0, 0);
            }

        }
@param {Array} tiledImages Array of TiledImage objects to draw