Merge pull request #3 from Aiosa/rotation-seams

Exposing matrix implementation, automatic drawer recognition

Thanks, @Aiosa!
This commit is contained in:
pearcetm 2023-07-26 19:28:29 -04:00 committed by GitHub
commit d912ff3196
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 208 additions and 149 deletions

View File

@ -28,6 +28,7 @@ module.exports = function(grunt) {
coverageDir = 'coverage/' + dateFormat(new Date(), 'yyyymmdd-HHMMss'),
sources = [
"src/openseadragon.js",
"src/matrix.js",
"src/fullscreen.js",
"src/eventsource.js",
"src/mousetracker.js",
@ -64,7 +65,15 @@ module.exports = function(grunt) {
"src/viewport.js",
"src/tiledimage.js",
"src/tilecache.js",
"src/world.js"
"src/world.js",
// Aiosa's webgl drawer - needs optimization, polishing, trimming
// "src/webgl/webGLWrapper.js",
// "src/webgl/visualisationLayer.js",
// "src/webgl/dataLoader.js",
// "src/webgl/webGLContext.js",
// "src/webgl/drawer.js",
// "src/webgl/plainShader.js",
];
var banner = "//! <%= pkg.name %> <%= pkg.version %>\n" +
@ -195,7 +204,7 @@ module.exports = function(grunt) {
}
},
watch: {
files: [ "Gruntfile.js", "src/*.js", "images/*" ],
files: [ "Gruntfile.js", "src/*.js", "images/*" /*, "src/webgl/*.js" */ ],
tasks: "watchTask"
},
eslint: {

View File

@ -246,7 +246,7 @@ class CanvasDrawer extends $.DrawerBase{
if (lastDrawn.length > 1 &&
imageZoom > tiledImage.smoothTileEdgesMinZoom &&
!tiledImage.iOSDevice &&
tiledImage.getRotation(true) % 360 === 0 ){ // TO DO: support tile edge smoothing with tiled image rotation.
tiledImage.getRotation(true) % 360 === 0 ){ // TODO: support tile edge smoothing with tiled image rotation.
// When zoomed in a lot (>100%) the tile edges are visible.
// So we have to composite them at ~100% and scale them up together.
// Note: Disabled on iOS devices per default as it causes a native crash

View File

@ -84,7 +84,7 @@ $.DrawerBase = class DrawerBase{
*/
this.container = $.getElement( options.element );
// TO DO: Does this need to be in DrawerBase, or only in Drawer implementations?
// TODO: Does this need to be in DrawerBase, or only in Drawer implementations?
// Original commment:
// We force our container to ltr because our drawing math doesn't work in rtl.
// This issue only affects our canvas renderer, but we do it always for consistency.
@ -108,9 +108,7 @@ $.DrawerBase = class DrawerBase{
this._checkForAPIOverrides();
}
get isOpenSeadragonDrawer(){
return true;
}
get canvas(){
if(!this._renderingTarget){
this._renderingTarget = this.createDrawingElement();

123
src/matrix.js Normal file
View File

@ -0,0 +1,123 @@
/*
* OpenSeadragon - Mat3
*
* Modified from https://webglfundamentals.org/webgl/lessons/webgl-2d-matrices.html
* Copyright (C) 2010-2023 webglfundamentals.org and 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( $ ){
/**
* Matrix left-to-right system representation
*/
$.Mat3 = class Mat3 {
constructor(values){
if(!values) {
values = [
0, 0, 0,
0, 0, 0,
0, 0, 0
];
}
this.values = values;
}
static makeIdentity(){
return new Mat3([
1, 0, 0,
0, 1, 0,
0, 0, 1
]);
}
static makeTranslation(tx, ty) {
return new Mat3([
1, 0, 0,
0, 1, 0,
tx, ty, 1,
]);
}
static makeRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return new Mat3([
c, -s, 0,
s, c, 0,
0, 0, 1,
]);
}
static makeScaling(sx, sy) {
return new Mat3([
sx, 0, 0,
0, sy, 0,
0, 0, 1,
]);
}
multiply(other) {
let a = this.values;
let b = other.values;
var a00 = a[0 * 3 + 0];
var a01 = a[0 * 3 + 1];
var a02 = a[0 * 3 + 2];
var a10 = a[1 * 3 + 0];
var a11 = a[1 * 3 + 1];
var a12 = a[1 * 3 + 2];
var a20 = a[2 * 3 + 0];
var a21 = a[2 * 3 + 1];
var a22 = a[2 * 3 + 2];
var b00 = b[0 * 3 + 0];
var b01 = b[0 * 3 + 1];
var b02 = b[0 * 3 + 2];
var b10 = b[1 * 3 + 0];
var b11 = b[1 * 3 + 1];
var b12 = b[1 * 3 + 2];
var b20 = b[2 * 3 + 0];
var b21 = b[2 * 3 + 1];
var b22 = b[2 * 3 + 2];
return new Mat3([
b00 * a00 + b01 * a10 + b02 * a20,
b00 * a01 + b01 * a11 + b02 * a21,
b00 * a02 + b01 * a12 + b02 * a22,
b10 * a00 + b11 * a10 + b12 * a20,
b10 * a01 + b11 * a11 + b12 * a21,
b10 * a02 + b11 * a12 + b12 * a22,
b20 * a00 + b21 * a10 + b22 * a20,
b20 * a01 + b21 * a11 + b22 * a21,
b20 * a02 + b21 * a12 + b22 * a22,
]);
}
};
}( OpenSeadragon ));

View File

@ -1473,7 +1473,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
* @param {Number} levelOpacity
* @param {Number} currentTime
* @param {Boolean} lowestLevel
* @returns {Boolean} whether the opacity of this tile has changed
* @returns {Boolean} true if blending did not yet finish
*/
_blendTile: function(tile, x, y, level, levelOpacity, currentTime, lowestLevel ){
let blendTimeMillis = 1000 * this.blendTime,

View File

@ -445,47 +445,44 @@ $.Viewer = function( options ) {
delete this.drawerOptions.useCanvas;
}
let drawerPriority = Array.isArray(this.drawer) ? this.drawer : [this.drawer];
let drawersToTry = drawerPriority.filter(d => ['webgl', 'canvas', 'html'].includes(d) || (d.prototype && d.prototype.isOpenSeadragonDrawer) );
if(drawerPriority.length !== drawersToTry.length){
$.console.error('An invalid drawer was requested.');
}
if(drawersToTry.length === 0){
drawersToTry = [$.DEFAULT_SETTINGS.drawer].flat(); // ensure it is a list
let drawerCandidates = Array.isArray(this.drawer) ? this.drawer : [this.drawer];
if (drawerCandidates.length === 0){
drawerCandidates = [$.DEFAULT_SETTINGS.drawer].flat(); // ensure it is a list
$.console.warn('No valid drawers were selected. Using the default value.');
}
// extend the drawerOptions object with additional properties to pass to the Drawer implementation
this.drawer = null; // TO DO: how to deal with the possibility that none of the requested drawers are supported?
for(let i = 0; i < drawersToTry.length; i++){
let Drawer = drawersToTry[i];
let optsKey = null;
// replace text-based option with appropriate constructor
if (Drawer === 'canvas'){
Drawer = $.CanvasDrawer;
optsKey = 'canvas';
} else if (Drawer === 'html'){
Drawer = $.HTMLDrawer;
optsKey = 'html';
} else if (Drawer === 'webgl'){
Drawer = $.WebGLDrawer;
optsKey = 'webgl';
// TODO: how to deal with the possibility that none of the requested drawers are supported?
this.drawer = null;
for (let i = 0; i < drawerCandidates.length; i++) {
let drawerCandidate = drawerCandidates[i];
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 {
optsKey = 'custom';
$.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.isSupported()){
if (Drawer.isSupported()) {
this.drawer = new Drawer({
viewer: this,
viewport: this.viewport,
element: this.canvas,
debugGridColor: this.debugGridColor,
options: this.drawerOptions[optsKey],
options: this.drawerOptions[drawerCandidate],
});
this.drawerOptions.constructor = Drawer;
break;
}
}
if(this.drawer === null){
if (!this.drawer){
$.console.error('No drawer could be created!');
throw('Error with creating the selected drawer(s)');
}
@ -4002,4 +3999,22 @@ function onFlip() {
this.viewport.toggleFlip();
}
/**
* Find drawer
*/
$.determineDrawer = function( id ){
for (let property in OpenSeadragon) {
const drawer = OpenSeadragon[ property ],
proto = drawer.prototype;
if( proto &&
proto instanceof OpenSeadragon.DrawerBase &&
$.isFunction( proto.getType ) &&
proto.getType.call( drawer ) === id
){
return drawer;
}
}
return null;
};
}( OpenSeadragon ));

View File

@ -35,92 +35,6 @@
(function( $ ){
// internal class Mat3: implements matrix operations
// Modified from https://webglfundamentals.org/webgl/lessons/webgl-2d-matrices.html
class Mat3{
constructor(values){
if(!values) {
values = [
0, 0, 0,
0, 0, 0,
0, 0, 0
];
}
this.values = values;
}
static makeIdentity(){
return new Mat3([
1, 0, 0,
0, 1, 0,
0, 0, 1
]);
}
static makeTranslation(tx, ty) {
return new Mat3([
1, 0, 0,
0, 1, 0,
tx, ty, 1,
]);
}
static makeRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return new Mat3([
c, -s, 0,
s, c, 0,
0, 0, 1,
]);
}
static makeScaling(sx, sy) {
return new Mat3([
sx, 0, 0,
0, sy, 0,
0, 0, 1,
]);
}
multiply(other) {
let a = this.values;
let b = other.values;
var a00 = a[0 * 3 + 0];
var a01 = a[0 * 3 + 1];
var a02 = a[0 * 3 + 2];
var a10 = a[1 * 3 + 0];
var a11 = a[1 * 3 + 1];
var a12 = a[1 * 3 + 2];
var a20 = a[2 * 3 + 0];
var a21 = a[2 * 3 + 1];
var a22 = a[2 * 3 + 2];
var b00 = b[0 * 3 + 0];
var b01 = b[0 * 3 + 1];
var b02 = b[0 * 3 + 2];
var b10 = b[1 * 3 + 0];
var b11 = b[1 * 3 + 1];
var b12 = b[1 * 3 + 2];
var b20 = b[2 * 3 + 0];
var b21 = b[2 * 3 + 1];
var b22 = b[2 * 3 + 2];
return new Mat3([
b00 * a00 + b01 * a10 + b02 * a20,
b00 * a01 + b01 * a11 + b02 * a21,
b00 * a02 + b01 * a12 + b02 * a22,
b10 * a00 + b11 * a10 + b12 * a20,
b10 * a01 + b11 * a11 + b12 * a21,
b10 * a02 + b11 * a12 + b12 * a22,
b20 * a00 + b21 * a10 + b22 * a20,
b20 * a01 + b21 * a11 + b22 * a21,
b20 * a02 + b21 * a12 + b22 * a22,
]);
}
}
/**
* @class WebGLDrawer
* @memberof OpenSeadragon
@ -198,7 +112,7 @@
// Delete all our created resources
gl.deleteBuffer(this._secondPass.bufferOutputPosition);
gl.deleteFramebuffer(this._glFrameBuffer);
// TO DO: if/when render buffers or frame buffers are used, release them:
// TODO: if/when render buffers or frame buffers are used, release them:
// gl.deleteRenderbuffer(someRenderbuffer);
// gl.deleteFramebuffer(someFramebuffer);
@ -277,9 +191,9 @@
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 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);
@ -300,7 +214,7 @@
tiledImage._croppingPolygons ||
tiledImage.debugMode
);
let useTwoPassRendering = useContext2dPipeline || (tiledImage.opacity < 1); // TO DO: check hasTransparency in addition to opacity
let useTwoPassRendering = useContext2dPipeline || (tiledImage.opacity < 1); // TODO: check hasTransparency in addition to opacity
let tilesToDraw = tiledImage.getTilesToDraw();
@ -343,10 +257,10 @@
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 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);
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);
@ -431,7 +345,7 @@
// Draw the quad (two triangles)
gl.drawArrays(gl.TRIANGLES, 0, 6);
// TO DO: is this the mechanism we want to use here?
// 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++){
@ -456,7 +370,7 @@
}
// Fire tiled-image-drawn event.
// TO DO: the image data may not be on the output canvas yet!!
// 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
@ -477,7 +391,7 @@
}
});
// TO DO: the line below is a test!
// TODO: the line below is a test!
if(renderingBufferHasImageData){
this._outputContext.drawImage(this._renderingCanvas, 0, 0);
}
@ -572,7 +486,7 @@
let w = right - x;
let h = bottom - y;
let matrix = new Mat3([
let matrix = new $.Mat3([
w, 0, 0,
0, h, 0,
x, y, 1,
@ -580,11 +494,11 @@
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);
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);
let localMatrix = t1.multiply($.Mat3.makeScaling(-1, 1)).multiply(t2);
matrix = matrix.multiply(localMatrix);
}
@ -596,7 +510,7 @@
if(this.continuousTileRefresh){
// Upload the image into the texture
// TO DO: test if this works appropriately
// TODO: test if this works appropriately
let tileContext = tile.getCanvasContext();
this._raiseTileDrawingEvent(tiledImage, this._outputContext, tile, tileContext);
this._uploadImageData(tileContext, tile, tiledImage);
@ -798,7 +712,7 @@
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));
let matrix = $.Mat3.makeScaling(2, 2).multiply($.Mat3.makeTranslation(-0.5, -0.5));
gl.uniformMatrix3fv(this._secondPass.uMatrix, false, matrix.values);
}
@ -957,8 +871,6 @@
} catch (e){
$.console.error('Error uploading image data to WebGL', e);
}
}
_imageUnloadedHandler(event){
@ -977,7 +889,7 @@
}
// release the position buffer from the GPU
// TO DO: do this!
// TODO: do this!
}
// private
// necessary for clip testing to pass (test uses spyOnce(drawer._setClip))

View File

@ -30,7 +30,10 @@ let viewer;
$('#create-drawer').on('click',function(){
let drawerType = $('#select-drawer').val();
let num = Math.floor($('#input-number').val());
run(drawerType, num);
});
function run(drawerType, num) {
if(viewer){
viewer.destroy();
}
@ -57,8 +60,7 @@ $('#create-drawer').on('click',function(){
viewer.viewport.panBy(new OpenSeadragon.Point( dist * m/2, 0));
}, 1000);
});
}
function makeViewer(drawerType){
let viewer = OpenSeadragon({

View File

@ -200,7 +200,7 @@
// Delete all our created resources
gl.deleteBuffer(this._secondPass.bufferOutputPosition);
gl.deleteFramebuffer(this._glFrameBuffer);
// TO DO: if/when render buffers or frame buffers are used, release them:
// TODO: if/when render buffers or frame buffers are used, release them:
// gl.deleteRenderbuffer(someRenderbuffer);
// gl.deleteFramebuffer(someFramebuffer);
@ -302,8 +302,8 @@
tiledImage._croppingPolygons ||
tiledImage.debugMode
);
let useTwoPassRendering = useContext2dPipeline || (tiledImage.opacity < 1); // TO DO: check hasTransparency in addition to opacity
let useTwoPassRendering = useContext2dPipeline ||(tiledImage.opacity < 1); // TODO: check hasTransparency in addition to opacity
let tilesToDraw = tiledImage.getTilesToDraw();
@ -433,7 +433,7 @@
// Draw the quad (two triangles)
gl.drawArrays(gl.TRIANGLES, 0, 6);
// TO DO: is this the mechanism we want to use here?
// 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++){
@ -458,7 +458,7 @@
}
// Fire tiled-image-drawn event.
// TO DO: the image data may not be on the output canvas yet!!
// 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
@ -479,7 +479,7 @@
}
});
// TO DO: the line below is a test!
// TODO: the line below is a test!
if(renderingBufferHasImageData){
this._outputContext.drawImage(this._renderingCanvas, 0, 0);
}
@ -598,7 +598,7 @@
if(this.continuousTileRefresh){
// Upload the image into the texture
// TO DO: test if this works appropriately
// TODO: test if this works appropriately
let tileContext = tile.getCanvasContext();
this._raiseTileDrawingEvent(tiledImage, this._outputContext, tile, tileContext);
this._uploadImageData(tileContext, tile, tiledImage);
@ -979,7 +979,7 @@
}
// release the position buffer from the GPU
// TO DO: do this!
// TODO: do this!
}
// private
// necessary for clip testing to pass (test uses spyOnce(drawer._setClip))