@@ -0,0 +1 @@ | |||
ko_fi: mpetroff |
@@ -9,3 +9,9 @@ utils/doc/generated_docs | |||
# Ignore IntelliJ Files | |||
.idea | |||
# Ignore logs | |||
tests/*.log | |||
# Ignore tests | |||
tests/** |
@@ -0,0 +1,13 @@ | |||
language: generic | |||
dist: xenial | |||
addons: | |||
sauce_connect: true | |||
before_install: | |||
- sudo apt-get install -y python3-pillow python3-numpy python3-pip | |||
- sudo pip3 install selenium | |||
jobs: | |||
include: | |||
- stage: build | |||
script: python3 utils/build/build.py | |||
- stage: test | |||
script: python3 tests/run_tests.py |
@@ -1,4 +1,4 @@ | |||
Copyright (c) 2011-2018 Matthew Petroff | |||
Copyright (c) 2011-2019 Matthew Petroff | |||
Permission is hereby granted, free of charge, to any person obtaining a copy of | |||
this software and associated documentation files (the "Software"), to deal in | |||
@@ -1 +1 @@ | |||
2.4.1 | |||
2.5.2 |
@@ -2,8 +2,68 @@ Changelog | |||
========= | |||
Changes in Pannellum 2.4.1 | |||
-------------------------- | |||
Changes in Pannellum 2.5.2 (2019-07-13) | |||
--------------------------------------- | |||
Bugfixes: | |||
- Fixed regression in Video.js plugin | |||
Changes in Pannellum 2.5.1 (2019-07-13) | |||
--------------------------------------- | |||
Other: | |||
- Fixed issue with tagging 2.5.0 release | |||
Changes in Pannellum 2.5.0 (2019-07-13) | |||
--------------------------------------- | |||
New Features: | |||
- The background color can be set for partial panoramas | |||
(`backgroundColor` parameter) | |||
- Partial panoramas are now supported for the multiresolution format | |||
- An author URL can now be added (`authorURL` parameter) | |||
New API functions: | |||
- Added `fullscreenchange`, `zoomchange`, and `animatefinished` events | |||
- Added `stopMovement` function for stopping all viewer movement | |||
Improvements: | |||
- Equirectangular images are now automatically split into two separate | |||
textures if they're too big (images up to 8192px wide should now be | |||
widely supported) | |||
- Improved render quality for equirectangular images on mobile (using `highp` | |||
for fragment shader) | |||
- Keyboard events for keys not used by the viewer are no longer captured, and | |||
the list of captured keys is configurable (`capturedKeyNumbers` parameter) | |||
- Multiresolution tiles can now be generated from cylindrical panoramas | |||
- Hot spots can now be removed from scenes that aren't currently loaded | |||
- Hot spot cursor is now set via CSS class (so it can be overridden) | |||
- Hot spot link attributes can now be set (`attributes` parameter) | |||
- The "friction" that slows down the viewer motion can now be configured | |||
(`friction` parameter) | |||
- Dynamic scenes are now properly supported for tours | |||
Bugfixes: | |||
- Fixed regression in fallback renderer | |||
- Fixed bug with URL encoding | |||
- Fixed regression in Video.js plugin | |||
- Fixed auto-rotate bug that was manifested when using API to set view | |||
- Fixed full screen bug in Chrome | |||
- Fixed bug with removing event listeners | |||
- Fixed issue with mouse dragging causing jump around yaw limits | |||
- Fixed bug with deleting hot spots | |||
- Fixed bug with fading between scenes | |||
Other: | |||
- Added limited test suite / continuous integration | |||
- Removed `requestAnimationFrame` shim, dropping support for some | |||
older browsers | |||
Changes in Pannellum 2.4.1 (2018-03-03) | |||
--------------------------------------- | |||
Bugfixes: | |||
@@ -11,8 +71,8 @@ Bugfixes: | |||
- The API's `loadScene` method now works when no scenes have been loaded yet | |||
Changes in Pannellum 2.4.0 | |||
-------------------------- | |||
Changes in Pannellum 2.4.0 (2018-01-30) | |||
--------------------------------------- | |||
New Features: | |||
@@ -77,16 +137,16 @@ Bugfixes: | |||
- Fixed bug related to removing hot spots | |||
Changes in Pannellum 2.3.2 | |||
-------------------------- | |||
Changes in Pannellum 2.3.2 (2016-11-20) | |||
--------------------------------------- | |||
Bugfixes: | |||
- Fix Chrome fullscreen regression introduced in 2.3.1 | |||
Changes in Pannellum 2.3.1 | |||
-------------------------- | |||
Changes in Pannellum 2.3.1 (2016-11-19) | |||
--------------------------------------- | |||
Bugfixes: | |||
@@ -99,8 +159,8 @@ Improvements: | |||
- Better handling of view limits when both limits are in view | |||
Changes in Pannellum 2.3.0 | |||
-------------------------- | |||
Changes in Pannellum 2.3.0 (2016-10-30) | |||
--------------------------------------- | |||
New Features: | |||
@@ -169,8 +229,8 @@ Backwards-Incompatible Configuration Parameter Changes: | |||
can be used with the `config` parameter | |||
Changes in Pannellum 2.2.1 | |||
-------------------------- | |||
Changes in Pannellum 2.2.1 (2016-03-11) | |||
--------------------------------------- | |||
New Features: | |||
@@ -183,8 +243,8 @@ Improvements: | |||
- Better restriction on yaw range | |||
Changes in Pannellum 2.2.0 | |||
-------------------------- | |||
Changes in Pannellum 2.2.0 (2016-01-27) | |||
--------------------------------------- | |||
New Features: | |||
@@ -251,15 +311,15 @@ Other: | |||
- Added JSDoc documentation | |||
Changes in Pannellum 2.1.1 | |||
-------------------------- | |||
Changes in Pannellum 2.1.1 (2015-01-19) | |||
--------------------------------------- | |||
Bugfixes: | |||
- Force subpixel rendering for hot spots | |||
Changes in Pannellum 2.1.0 | |||
-------------------------- | |||
Changes in Pannellum 2.1.0 (2015-01-14) | |||
--------------------------------------- | |||
New Features: | |||
@@ -301,15 +361,15 @@ Other: | |||
removed | |||
Changes in Pannellum 2.0.1 | |||
-------------------------- | |||
Changes in Pannellum 2.0.1 (2014-08-24) | |||
--------------------------------------- | |||
Bugfixes: | |||
- Fix keyboard controls in Safari | |||
Changes in Pannellum 2.0 | |||
------------------------ | |||
Changes in Pannellum 2.0 (2014-08-22) | |||
------------------------------------- | |||
New Features: | |||
@@ -334,8 +394,8 @@ Bugfixes: | |||
- Numerous | |||
Changes in Pannellum 1.2 | |||
------------------------ | |||
Changes in Pannellum 1.2 (2012-08-28) | |||
------------------------------------- | |||
New Features: | |||
@@ -23,6 +23,13 @@ If set, the value is displayed as the panorama's author. If no author is | |||
desired, don't set this parameter. | |||
### `authorURL` (string) | |||
If set, the displayed author text is hyperlinked to this URL. If no author URL | |||
is desired, don't set this parameter. The `author` parameter must also be set | |||
for this parameter to have an effect. | |||
### `strings` (dictionary) | |||
Allows user-facing strings to be changed / translated. | |||
@@ -52,7 +59,8 @@ counter-clockwise, and negative is clockwise. | |||
Sets the delay, in milliseconds, to start automatically rotating the panorama | |||
after user activity ceases. This parameter only has an effect if the | |||
`autoRotate` parameter is set. | |||
`autoRotate` parameter is set. Before starting rotation, the viewer is panned | |||
to the initial pitch. | |||
### `autoRotateStopDelay` (number) | |||
@@ -308,6 +316,13 @@ If `clickHandlerFunc` is specified, this function is added as an event handler | |||
for the hot spot's `click` event. The event object and the contents of | |||
`clickHandlerArgs` are passed to the function as arguments. | |||
#### `scale` (boolean) | |||
When `true`, the hot spot is scaled to match changes in the field of view, | |||
relative to the initial field of view. Note that this does not account for | |||
changes in local image scale that occur due to distortions within the viewport. | |||
Defaults to `false`. | |||
### `hotSpotDebug` (boolean) | |||
When `true`, the mouse pointer's pitch and yaw are logged to the console when | |||
@@ -1,7 +1,7 @@ | |||
{ | |||
"name": "pannellum", | |||
"description": "Pannellum is a lightweight, free, and open source panorama viewer for the web.", | |||
"version": "2.4.1", | |||
"version": "2.5.2", | |||
"bugs": { | |||
"url": "https://github.com/mpetroff/pannellum/issues" | |||
}, | |||
@@ -0,0 +1,100 @@ | |||
@inproceedings{Chen1995, | |||
doi = {10.1145/218380.218395}, | |||
year = {1995}, | |||
publisher = {{ACM} Press}, | |||
author = {Shenchang Eric Chen}, | |||
title = {{QuickTime} {VR}}, | |||
editor = {Susan G. Mair and Robert Cook}, | |||
booktitle = {Proceedings of the 22nd annual conference on computer graphics and interactive techniques ({SIGGRAPH} '95)}, | |||
} | |||
@inproceedings{Gede2015, | |||
author = {M\'{a}ty\'{a}s Gede and Zsuzsanna Ungv\'{a}ri and Klaudia Kiss and G\'{a}bor Nagy}, | |||
title = {Open-source web-based viewer application for {TLS} surveys in caves}, | |||
booktitle = "Proceedings of the 1st {ICA} {European} Symposium on Cartography ({EuroCarto} 2015)", | |||
year = {2015}, | |||
editor = {Georg Gartner and Haosheng Huang}, | |||
pages = {321--328}, | |||
url = {https://cartography.tuwien.ac.at/eurocarto/wp-content/uploads/2015/10/6-7.pdf}, | |||
urldate = {2019-07-14}, | |||
isbn = {9781907075032}, | |||
} | |||
@article{Srinivasan2018, | |||
doi = {10.23925/1980-7651.2018v21;p71-83}, | |||
year = {2018}, | |||
month = jun, | |||
publisher = {Portal de Revistas {PUC} {SP}}, | |||
volume = {21}, | |||
pages = {71}, | |||
author = {Venkat Srinivasan and T.B. Dinesh and Bhanu Prakash and A. Shalini}, | |||
title = {Thirteen ways of looking at institutional history: a model for digital exhibitions from science archives}, | |||
journal = {Circumscribere: International Journal for the History of Science}, | |||
} | |||
@article{Herault2018, | |||
doi = {10.1186/s40561-018-0074-x}, | |||
year = {2018}, | |||
month = oct, | |||
publisher = {Springer Nature}, | |||
volume = {5}, | |||
number = {1}, | |||
author = {Romain Christian Herault and Alisa Lincke and Marcelo Milrad and Elin-Sofie Forsg\"{a}rde and Carina Elmqvist}, | |||
title = {Using 360-degrees interactive videos in patient trauma treatment education: design, development and evaluation aspects}, | |||
journal = {Smart Learning Environments}, | |||
} | |||
@incollection{Mohr2018, | |||
doi = {10.1007/978-3-030-04028-4_71}, | |||
year = {2018}, | |||
publisher = {Springer International Publishing}, | |||
pages = {613--620}, | |||
author = {Fabian Mohr and Soenke Zehle and Michael Schmitz}, | |||
editor = {Rebecca Rouse and Hartmut Koenitz and Mads Haahr}, | |||
title = {From Co-Curation to Co-Creation: Users as Collective Authors of Archive-Based Cultural Heritage Narratives}, | |||
booktitle = {Interactive Storytelling}, | |||
} | |||
@inproceedings{Albrizio2013, | |||
author = {Patrizia Albrizio and Francesco de Virgilio and Ginevra Panzarino and Enrica Zambetta}, | |||
title = {WebGIS e divulgazione del dato archeologico con software open source. Il progetto ``Siponto Aperta''}, | |||
booktitle = {Proceedings of {ArcheoFOSS}: free, libre and open source software e open format nei processi di ricerca archeologica}, | |||
year = {2013}, | |||
editor = {Filippo Stanco and Giovanni Gallo}, | |||
pages = {101--114}, | |||
edition = {8th}, | |||
url = {https://www.archaeopress.com/ArchaeopressShop/Public/download.asp?id=%7B14C6CFBD-3371-4DF0-8971-D4ABC24E661E%7D}, | |||
urldate = {2019-07-14}, | |||
isbn = {9781784912598}, | |||
} | |||
@online{ESO2017, | |||
author = {{European Southern Observatory}}, | |||
title = {A panorama view of the {VLT}}, | |||
year = {2017}, | |||
month = sep, | |||
url = {https://www.eso.org/public/images/165309674464e758889a6_eq-ext/}, | |||
urldate = {2019-07-14}, | |||
} | |||
@techreport{WebGL, | |||
author = {Dean Jackson}, | |||
title = {{WebGL} Specification}, | |||
month = oct, | |||
url = {https://www.khronos.org/registry/webgl/specs/1.0.3/}, | |||
urldate = {2019-07-14}, | |||
year = {2014}, | |||
type = {Khronos Specification}, | |||
institution = {Khronos Group}, | |||
} | |||
@techreport{Canvas, | |||
author = {Rik Cabanier and Jatinder Mann and Jay Munro and Tom Wiltzius and Ian Hickson}, | |||
title = {{HTML} Canvas {2D} Context}, | |||
month = nov, | |||
url = {https://www.w3.org/TR/2015/REC-2dcontext-20151119/}, | |||
urldate = {2019-07-14}, | |||
year = {2015}, | |||
type = {{W3C} Recommendation}, | |||
institution = {World Wide Web Consortium ({W3C})}, | |||
} |
@@ -0,0 +1,62 @@ | |||
--- | |||
title: 'Pannellum: a lightweight web-based panorama viewer' | |||
tags: | |||
- panoramas | |||
- visualization | |||
- WebGL | |||
authors: | |||
- name: Matthew A. Petroff | |||
orcid: 0000-0002-4436-4215 | |||
affiliation: 1 | |||
affiliations: | |||
- name: Department of Physics & Astronomy, Johns Hopkins University, Baltimore, Maryland 21218, USA | |||
index: 1 | |||
date: 15 July 2019 | |||
bibliography: paper.bib | |||
--- | |||
# Summary | |||
_Pannellum_ is an interactive web browser-based panorama viewer written in | |||
JavaScript and primarily based on the WebGL web standard [@WebGL] for graphics | |||
processing unit (GPU)-accelerated rendering to the HTML5 ``<canvas>`` element | |||
[@Canvas]. It supports the display of panoramic images that cover the full | |||
sphere, or only parts of it, in equirectangular format, in cube map format, or | |||
in a tiled format that encodes the panorama in multiple resolutions, which | |||
allows for parts of the panorama to be dynamically loaded, reducing data | |||
transfer requirements. In addition to single panoramas, multiple panoramas can | |||
be linked together into a virtual tour, with navigation enabled via | |||
"hot spots," which can also be used to add annotations. | |||
The display of interactive panoramic images on the web dates back to the | |||
mid-1990s, with the development of Apple's QuickTime VR format and associated | |||
web browser plug-ins [@Chen1995]. When development on _Pannellum_ started in | |||
2011, WebGL was a nascent technology, and the majority of existing panorama | |||
viewers for websites were then still based on Java or Adobe Flash plug-ins, | |||
which had supplanted QuickTime as the technology of choice. Since then, both | |||
the viewer and underlying technologies have matured immensely. | |||
An application programming interface (API) is provided, which allows external | |||
code to control the viewer and implement features such as custom buttons or | |||
integration with other web page elements, e.g., maps [@Gede2015; @Albrizio2013]. | |||
Panoramic videos are supported via a bundled extension, which is built using | |||
the API. The underlying rendering code is separate from the user interface | |||
code, which allows for more extensive customization and tighter integration | |||
with external code, if desired. This rendering code uses a pinhole camera model | |||
for equirectangular panoramas implemented as a WebGL fragment shader, instead | |||
of the more common---and less accurate---approach of mapping the panorama onto | |||
a geometric approximation of a sphere. | |||
_Pannellum_ has proven useful in various fields, when the display of panoramic | |||
images is needed to help digest or present information. These research | |||
applications range from cartography [@Gede2015] to digital humanities | |||
[@Srinivasan2018; @Mohr2018] to archaeology [@Albrizio2013] to medical | |||
education [@Herault2018]. It has also found use in public outreach | |||
applications, such as its use by the European Southern Observatory to display | |||
panoramas of their observatories [@ESO2017]. _Pannellum_ is intended to be used | |||
any time an interactive panorama needs to be displayed in a web page, be it an | |||
internal research application or a publicly accessible website. It may also | |||
work with certain mobile application frameworks, but such use is not officially | |||
supported. | |||
# References |
@@ -1,8 +1,11 @@ | |||
# Pannellum | |||
[![Build Status](https://travis-ci.org/mpetroff/pannellum.svg?branch=master)](https://travis-ci.org/mpetroff/pannellum) | |||
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.3334433.svg)](https://doi.org/10.5281/zenodo.3334433) | |||
## About | |||
Pannellum is a lightweight, free, and open source panorama viewer for the web. Built using HTML5, CSS3, JavaScript, and WebGL, it is plug-in free. It can be deployed easily as a single file, just 15kB gzipped, and then embedded into pages as an `<iframe>`. A configuration utility is included to generate the required code for embedding. | |||
Pannellum is a lightweight, free, and open source panorama viewer for the web. Built using HTML5, CSS3, JavaScript, and WebGL, it is plug-in free. It can be deployed easily as a single file, just 21kB gzipped, and then embedded into pages as an `<iframe>`. A configuration utility is included to generate the required code for embedding. An API is included for more advanced integrations. | |||
## How to use | |||
1. Upload `build/pannellum.htm` and a full equirectangular panorama to a web server. | |||
@@ -10,6 +13,8 @@ Pannellum is a lightweight, free, and open source panorama viewer for the web. B | |||
2. Use the included multi-resolution generator (`utils/multires/generate.py`) or configuration tool (`utils/config/configuration.htm`). | |||
3. Insert the generated `<iframe>` code into a page. | |||
Configuration parameters are documented in the `doc/json-config-parameters.md` file, which is also available at [pannellum.org/documentation/reference/](https://pannellum.org/documentation/reference). API methods are documented inline with [JSDoc](https://jsdoc.app/) comments, and generated documentation is available at [pannellum.org/documentation/api/](https://pannellum.org/documentation/api/). | |||
### Using `generate.py` to create multires panoramas | |||
To be able to create multiresolution panoramas, you need to have the `nona` program installed, which is available as part of [Hugin](http://hugin.sourceforge.net/), as well as Python with the [Pillow](https://pillow.readthedocs.org/) package. Then, run | |||
@@ -21,29 +26,20 @@ in the `utils/multires` directory. This will generate all the image tiles and th | |||
## Examples | |||
Examples using both the minified version and the version in the `src` directory are included in the `examples` directory. | |||
Examples using both the minified version and the version in the `src` directory are included in the `examples` directory. Additional examples are available at [pannellum.org](https://pannellum.org/documentation/examples/simple-example/). | |||
## Browser Compatibility | |||
Since Pannellum is built with recent web standards, it requires a modern browser to function. | |||
Since Pannellum is built with web standards, it requires a modern browser to function. | |||
#### Full support (with appropriate graphics drivers): | |||
* Firefox 10+ | |||
* Chrome 15+ | |||
* Firefox 23+ | |||
* Chrome 24+ | |||
* Safari 8+ | |||
* Internet Explorer 11+ | |||
* Edge | |||
#### Almost full support (no full screen): | |||
* Firefox 4+ | |||
* Chrome 9+ | |||
#### Partial support (WebGL support must first be enabled in preferences) | |||
* Safari 5.1+ | |||
#### No support: | |||
Internet Explorer 10 and previous | |||
The support list is based on feature support. As only recent browsers are tested, there may be regressions in older browsers. | |||
#### Not officially supported: | |||
@@ -56,6 +52,12 @@ All user-facing strings can be changed using the `strings` configuration paramet | |||
## Building | |||
The `utils` folder contains the required build tools, with the exception of Python 3.2+ and Java installations. To build a minified version of Pannellum, run either `build.sh` or `build.bat` depending on your platform. | |||
## Tests | |||
A limited [Selenium](https://www.seleniumhq.org/)-based test suite is located in the `tests` directory. The tests can be executed by running `python3 run_tests.py`. Running the tests requires Python 3, the Selenium Python bindings, Firefox, [geckodriver](https://github.com/mozilla/geckodriver), [Pillow](https://pillow.readthedocs.io/), and [NumPy](https://www.numpy.org/). | |||
## Contributing | |||
Development takes place at [github.com/mpetroff/pannellum](https://github.com/mpetroff/pannellum). Issues should be opened to report bugs or suggest improvements (or ask questions), and pull requests are welcome. When reporting a bug, please try to include a minimum reproducible example (or at least some sort of example). When proposing changes, please try to match the existing code style, e.g., four space indentation and [JSHint](https://jshint.com/) validation. If your pull request adds an additional configuration parameter, please document it in `doc/json-config-parameters.md`. | |||
## License | |||
Pannellum is distributed under the MIT License. For more information, read the file `COPYING` or peruse the license [online](https://github.com/mpetroff/pannellum/blob/master/COPYING). | |||
@@ -287,7 +287,7 @@ | |||
table-layout: fixed; | |||
} | |||
.pnlm-info-box a { | |||
.pnlm-info-box a, .pnlm-author-box a { | |||
color: #fff; | |||
word-wrap: break-word; | |||
overflow-wrap: break-word; | |||
@@ -1,22 +0,0 @@ | |||
/** | |||
* Provides requestAnimationFrame in a cross browser way. | |||
* http://paulirish.com/2011/requestanimationframe-for-smart-animating/ | |||
*/ | |||
if ( !window.requestAnimationFrame ) { | |||
window.requestAnimationFrame = ( function() { | |||
return window.webkitRequestAnimationFrame || | |||
window.mozRequestAnimationFrame || | |||
window.oRequestAnimationFrame || | |||
window.msRequestAnimationFrame || | |||
function( /* function FrameRequestCallback */ callback, /* DOMElement Element */ element ) { | |||
window.setTimeout( callback, 1000 / 60 ); | |||
}; | |||
} )(); | |||
} |
@@ -1,6 +1,6 @@ | |||
/* | |||
* libpannellum - A WebGL and CSS 3D transform based Panorama Renderer | |||
* Copyright (c) 2012-2018 Matthew Petroff | |||
* Copyright (c) 2012-2019 Matthew Petroff | |||
* | |||
* Permission is hereby granted, free of charge, to any person obtaining a copy | |||
* of this software and associated documentation files (the "Software"), to deal | |||
@@ -270,9 +270,9 @@ function Renderer(container) { | |||
faceImg.onload = onLoad; | |||
faceImg.onerror = incLoaded; // ignore missing face to support partial fallback image | |||
if (imageType == 'multires') { | |||
faceImg.src = encodeURI(path.replace('%s', sides[s]) + '.' + image.extension); | |||
faceImg.src = path.replace('%s', sides[s]) + '.' + image.extension; | |||
} else { | |||
faceImg.src = encodeURI(image[s].src); | |||
faceImg.src = image[s].src; | |||
} | |||
} | |||
fillMissingFaces(fallbackImgSize); | |||
@@ -308,9 +308,9 @@ function Renderer(container) { | |||
} | |||
} else if (imageType == 'cubemap') { | |||
if (cubeImgWidth > gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE)) { | |||
console.log('Error: The image is too big; it\'s ' + width + 'px wide, '+ | |||
console.log('Error: The image is too big; it\'s ' + cubeImgWidth + 'px wide, ' + | |||
'but this device\'s maximum supported size is ' + maxWidth + 'px.'); | |||
throw {type: 'webgl size error', width: width, maxWidth: maxWidth}; | |||
throw {type: 'webgl size error', width: cubeImgWidth, maxWidth: maxWidth}; | |||
} | |||
} | |||
@@ -325,6 +325,15 @@ function Renderer(container) { | |||
// Create viewport for entire canvas | |||
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); | |||
// Check precision support | |||
if (gl.getShaderPrecisionFormat) { | |||
var precision = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT); | |||
if (precision && precision.precision < 1) { | |||
// `highp` precision not supported; https://stackoverflow.com/a/33308927 | |||
fragEquiCubeBase = fragEquiCubeBase.replace('highp', 'mediump'); | |||
} | |||
} | |||
// Create vertex shader | |||
vs = gl.createShader(gl.VERTEX_SHADER); | |||
var vertexSrc = v; | |||
@@ -761,9 +770,9 @@ function Renderer(container) { | |||
program.nodeCache.length > program.currentNodes.length + 50) { | |||
// Remove older nodes from cache | |||
var removed = program.nodeCache.splice(200, program.nodeCache.length - 200); | |||
for (var i = 0; i < removed.length; i++) { | |||
for (var j = 0; j < removed.length; j++) { | |||
// Explicitly delete textures | |||
gl.deleteTexture(removed[i].texture); | |||
gl.deleteTexture(removed[j].texture); | |||
} | |||
} | |||
program.currentNodes = []; | |||
@@ -1591,7 +1600,7 @@ function Renderer(container) { | |||
}); | |||
this.image.addEventListener('load', loadFn); | |||
this.image.addEventListener('error', loadFn); // ignore missing tile file to support partial image, otherwise retry loop causes high CPU load | |||
}; | |||
} | |||
TextureImageLoader.prototype.loadTexture = function(node, src, texture, callback) { | |||
this.texture = texture; | |||
@@ -1616,7 +1625,7 @@ function Renderer(container) { | |||
this.src = src; | |||
this.texture = texture; | |||
this.callback = callback; | |||
}; | |||
} | |||
function releaseTextureImageLoader(til) { | |||
if (pendingTextureRequests.length) { | |||
@@ -1646,7 +1655,7 @@ function Renderer(container) { | |||
* @param {MultiresNode} node - Input node. | |||
*/ | |||
function processNextTile(node) { | |||
loadTexture(node, image.loader || node.uri, function (texture, loaded) { | |||
loadTexture(node, image.loader || node.path + '.' + image.extension, function (texture, loaded) { | |||
node.texture = texture; | |||
node.textureLoaded = loaded ? 2 : 1; | |||
@@ -1844,7 +1853,7 @@ var vMulti = [ | |||
// Fragment shader | |||
var fragEquiCubeBase = [ | |||
'precision mediump float;', | |||
'precision highp float;', // mediump looks bad on some mobile devices | |||
'uniform float u_aspectRatio;', | |||
'uniform float u_psi;', | |||
@@ -1,6 +1,6 @@ | |||
/* | |||
* Pannellum - An HTML5 based Panorama Viewer | |||
* Copyright (c) 2011-2018 Matthew Petroff | |||
* Copyright (c) 2011-2019 Matthew Petroff | |||
* | |||
* Permission is hereby granted, free of charge, to any person obtaining a copy | |||
* of this software and associated documentation files (the "Software"), to deal | |||
@@ -137,7 +137,7 @@ defaultConfig.strings = { | |||
'%spx wide. Try another device.' + | |||
' (If you\'re the author, try scaling down the image.)', // Two substitutions: image width, max image width | |||
unknownError: 'Unknown error. Check developer console.', | |||
} | |||
}; | |||
// Initialize container | |||
container = typeof container === 'string' ? document.getElementById(container) : container; | |||
@@ -341,6 +341,7 @@ function init() { | |||
} else { | |||
if (config.panorama === undefined) { | |||
anError(config.strings.noPanoramaError); | |||
loaded = undefined; | |||
return; | |||
} | |||
panoImage = new Image(); | |||
@@ -458,6 +459,13 @@ function init() { | |||
if (config.draggable) | |||
uiContainer.classList.add('pnlm-grab'); | |||
uiContainer.classList.remove('pnlm-grabbing'); | |||
// Properly handle switching to dynamic scenes | |||
update = config.dynamicUpdate === true; | |||
if (config.dynamic && update) { | |||
panoImage = config.panorama; | |||
onImageLoad(); | |||
} | |||
} | |||
/** | |||
@@ -469,7 +477,7 @@ function init() { | |||
function absoluteURL(url) { | |||
// From http://stackoverflow.com/a/19709846 | |||
return new RegExp('^(?:[a-z]+:)?//', 'i').test(url) || url[0] == '/' || url.slice(0, 5) == 'blob:'; | |||
}; | |||
} | |||
/** | |||
* Create renderer and initialize event listeners once image is loaded. | |||
@@ -649,6 +657,7 @@ function clearError() { | |||
infoDisplay.load.box.style.display = 'none'; | |||
infoDisplay.errorMsg.style.display = 'none'; | |||
error = false; | |||
renderContainer.style.display = 'block'; | |||
fireEvent('errorcleared'); | |||
} | |||
} | |||
@@ -1250,12 +1259,11 @@ function keyRepeat() { | |||
latestInteraction = Date.now(); | |||
// If auto-rotate | |||
var inactivityInterval = Date.now() - latestInteraction; | |||
if (config.autoRotate) { | |||
// Pan | |||
if (newTime - prevTime > 0.001) { | |||
var timeDiff = (newTime - prevTime) / 1000; | |||
var yawDiff = (speed.yaw / timeDiff * diff - config.autoRotate * 0.2) * timeDiff | |||
var yawDiff = (speed.yaw / timeDiff * diff - config.autoRotate * 0.2) * timeDiff; | |||
yawDiff = (-config.autoRotate > 0 ? 1 : -1) * Math.min(Math.abs(config.autoRotate * timeDiff), Math.abs(yawDiff)); | |||
config.yaw += yawDiff; | |||
} | |||
@@ -1439,6 +1447,17 @@ function render() { | |||
var tmpyaw; | |||
if (loaded) { | |||
var canvas = renderer.getCanvas(); | |||
if (config.autoRotate !== false) { | |||
// When auto-rotating this check needs to happen first (see issue #764) | |||
if (config.yaw > 180) { | |||
config.yaw -= 360; | |||
} else if (config.yaw < -180) { | |||
config.yaw += 360; | |||
} | |||
} | |||
// Keep a tmp value of yaw for autoRotate comparison later | |||
tmpyaw = config.yaw; | |||
@@ -1446,8 +1465,7 @@ function render() { | |||
var hoffcut = 0, | |||
voffcut = 0; | |||
if (config.avoidShowingBackground) { | |||
var canvas = renderer.getCanvas(), | |||
hfov2 = config.hfov / 2, | |||
var hfov2 = config.hfov / 2, | |||
vfov2 = Math.atan2(Math.tan(hfov2 / 180 * Math.PI), (canvas.width / canvas.height)) * 180 / Math.PI, | |||
transposed = config.vaov > config.haov; | |||
if (transposed) { | |||
@@ -1473,10 +1491,14 @@ function render() { | |||
config.yaw = Math.max(minYaw, Math.min(maxYaw, config.yaw)); | |||
} | |||
if (config.yaw > 180) { | |||
config.yaw -= 360; | |||
} else if (config.yaw < -180) { | |||
config.yaw += 360; | |||
if (!(config.autoRotate !== false)) { | |||
// When not auto-rotating, this check needs to happen after the | |||
// previous check (see issue #698) | |||
if (config.yaw > 180) { | |||
config.yaw -= 360; | |||
} else if (config.yaw < -180) { | |||
config.yaw += 360; | |||
} | |||
} | |||
// Check if we autoRotate in a limited by min and max yaw | |||
@@ -1487,7 +1509,6 @@ function render() { | |||
} | |||
// Ensure the calculated pitch is within min and max allowed | |||
var canvas = renderer.getCanvas(); | |||
var vfov = config.vfov; | |||
var minPitch = config.minPitch + vfov / 2, | |||
maxPitch = config.maxPitch - vfov / 2; | |||
@@ -1541,7 +1562,7 @@ Quaternion.prototype.multiply = function(q) { | |||
this.x*q.w + this.w*q.x + this.y*q.z - this.z*q.y, | |||
this.y*q.w + this.w*q.y + this.z*q.x - this.x*q.z, | |||
this.z*q.w + this.w*q.z + this.x*q.y - this.y*q.x); | |||
} | |||
}; | |||
/** | |||
* Converts quaternion to Euler angles. | |||
@@ -1555,7 +1576,7 @@ Quaternion.prototype.toEulerAngles = function() { | |||
psi = Math.atan2(2 * (this.w * this.z + this.x * this.y), | |||
1 - 2 * (this.y * this.y + this.z * this.z)); | |||
return [phi, theta, psi]; | |||
} | |||
}; | |||
/** | |||
* Converts device orientation API Tait-Bryan angles to a quaternion. | |||
@@ -1710,7 +1731,7 @@ function createHotSpot(hs) { | |||
hs.yaw = Number(hs.yaw) || 0; | |||
var div = document.createElement('div'); | |||
div.className = 'pnlm-hotspot-base' | |||
div.className = 'pnlm-hotspot-base'; | |||
if (hs.cssClass) | |||
div.className += ' ' + hs.cssClass; | |||
else | |||
@@ -1723,24 +1744,24 @@ function createHotSpot(hs) { | |||
var a; | |||
if (hs.video) { | |||
var video = document.createElement('video'), | |||
p = hs.video; | |||
if (config.basePath && !absoluteURL(p)) | |||
p = config.basePath + p; | |||
video.src = sanitizeURL(p); | |||
vidp = hs.video; | |||
if (config.basePath && !absoluteURL(vidp)) | |||
vidp = config.basePath + vidp; | |||
video.src = sanitizeURL(vidp); | |||
video.controls = true; | |||
video.style.width = hs.width + 'px'; | |||
renderContainer.appendChild(div); | |||
span.appendChild(video); | |||
} else if (hs.image) { | |||
var p = hs.image; | |||
if (config.basePath && !absoluteURL(p)) | |||
p = config.basePath + p; | |||
var imgp = hs.image; | |||
if (config.basePath && !absoluteURL(imgp)) | |||
imgp = config.basePath + imgp; | |||
a = document.createElement('a'); | |||
a.href = sanitizeURL(hs.URL ? hs.URL : p); | |||
a.href = sanitizeURL(hs.URL ? hs.URL : imgp); | |||
a.target = '_blank'; | |||
span.appendChild(a); | |||
var image = document.createElement('img'); | |||
image.src = sanitizeURL(p); | |||
image.src = sanitizeURL(imgp); | |||
image.style.width = hs.width + 'px'; | |||
image.style.paddingTop = '5px'; | |||
renderContainer.appendChild(div); | |||
@@ -1804,7 +1825,7 @@ function createHotSpot(hs) { | |||
} | |||
hs.div = div; | |||
}; | |||
} | |||
/** | |||
* | |||
@@ -1898,6 +1919,9 @@ function renderHotSpot(hs) { | |||
coord[1] += (canvasHeight - hs.div.offsetHeight) / 2; | |||
var transform = 'translate(' + coord[0] + 'px, ' + coord[1] + | |||
'px) translateZ(9999px) rotate(' + config.roll + 'deg)'; | |||
if (hs.scale) { | |||
transform += ' scale(' + (origHfov/config.hfov) / z + ')'; | |||
} | |||
hs.div.style.webkitTransform = transform; | |||
hs.div.style.MozTransform = transform; | |||
hs.div.style.transform = transform; | |||
@@ -2042,7 +2066,15 @@ function processOptions(isPreview) { | |||
break; | |||
case 'author': | |||
infoDisplay.author.innerHTML = config.strings.bylineLabel.replace('%s', escapeHTML(config[key])); | |||
var authorText = escapeHTML(config[key]); | |||
if (config.authorURL) { | |||
var authorLink = document.createElement('a'); | |||
authorLink.href = sanitizeURL(config['authorURL']); | |||
authorLink.target = '_blank'; | |||
authorLink.innerHTML = escapeHTML(config[key]); | |||
authorText = authorLink.outerHTML; | |||
} | |||
infoDisplay.author.innerHTML = config.strings.bylineLabel.replace('%s', authorText); | |||
infoDisplay.container.style.display = 'inline'; | |||
break; | |||
@@ -2052,7 +2084,7 @@ function processOptions(isPreview) { | |||
link.target = '_blank'; | |||
link.textContent = 'Click here to view this panorama in an alternative viewer.'; | |||
var message = document.createElement('p'); | |||
message.textContent = 'Your browser does not support WebGL.' | |||
message.textContent = 'Your browser does not support WebGL.'; | |||
message.appendChild(document.createElement('br')); | |||
message.appendChild(link); | |||
infoDisplay.errorMsg.innerHTML = ''; // Removes all children nodes | |||
@@ -2230,7 +2262,7 @@ function constrainHfov(hfov) { | |||
} | |||
if (minHfov > config.maxHfov) { | |||
// Don't change view if bounds don't make sense | |||
console.log('HFOV bounds do not make sense (minHfov > maxHfov).') | |||
console.log('HFOV bounds do not make sense (minHfov > maxHfov).'); | |||
return config.hfov; | |||
} | |||
var newHfov = config.hfov; | |||
@@ -2246,8 +2278,7 @@ function constrainHfov(hfov) { | |||
var canvas = renderer.getCanvas(); | |||
newHfov = Math.min(newHfov, | |||
Math.atan(Math.tan((config.maxPitch - config.minPitch) / 360 * Math.PI) / | |||
canvas.height * canvas.width) | |||
* 360 / Math.PI); | |||
canvas.height * canvas.width) * 360 / Math.PI); | |||
} | |||
return newHfov; | |||
} | |||
@@ -2355,6 +2386,8 @@ function load() { | |||
* @param {boolean} [fadeDone] - If `true`, fade setup is skipped. | |||
*/ | |||
function loadScene(sceneId, targetPitch, targetYaw, targetHfov, fadeDone) { | |||
if (!loaded) | |||
fadeDone = true; // Don't try to fade when there isn't a scene loaded | |||
loaded = false; | |||
animatedMove = {}; | |||
@@ -2419,13 +2452,6 @@ function loadScene(sceneId, targetPitch, targetYaw, targetHfov, fadeDone) { | |||
} | |||
fireEvent('scenechange', sceneId); | |||
load(); | |||
// Properly handle switching to dynamic scenes | |||
update = config.dynamicUpdate === true; | |||
if (config.dynamic) { | |||
panoImage = config.panorama; | |||
onImageLoad(); | |||
} | |||
} | |||
/** | |||
@@ -2538,9 +2564,9 @@ this.setPitch = function(pitch, animated, callback, callbackArgs) { | |||
'startPosition': config.pitch, | |||
'endPosition': pitch, | |||
'duration': animated | |||
} | |||
}; | |||
if (typeof callback == 'function') | |||
setTimeout(function(){callback(callbackArgs)}, animated); | |||
setTimeout(function(){callback(callbackArgs);}, animated); | |||
} else { | |||
config.pitch = pitch; | |||
} | |||
@@ -2599,22 +2625,22 @@ this.setYaw = function(yaw, animated, callback, callbackArgs) { | |||
return this; | |||
} | |||
animated = animated == undefined ? 1000: Number(animated); | |||
yaw = ((yaw + 180) % 360) - 180 // Keep in bounds | |||
yaw = ((yaw + 180) % 360) - 180; // Keep in bounds | |||
if (animated) { | |||
// Animate in shortest direction | |||
if (config.yaw - yaw > 180) | |||
yaw += 360 | |||
yaw += 360; | |||
else if (yaw - config.yaw > 180) | |||
yaw -= 360 | |||
yaw -= 360; | |||
animatedMove.yaw = { | |||
'startTime': Date.now(), | |||
'startPosition': config.yaw, | |||
'endPosition': yaw, | |||
'duration': animated | |||
} | |||
}; | |||
if (typeof callback == 'function') | |||
setTimeout(function(){callback(callbackArgs)}, animated); | |||
setTimeout(function(){callback(callbackArgs);}, animated); | |||
} else { | |||
config.yaw = yaw; | |||
} | |||
@@ -2679,9 +2705,9 @@ this.setHfov = function(hfov, animated, callback, callbackArgs) { | |||
'startPosition': config.hfov, | |||
'endPosition': constrainHfov(hfov), | |||
'duration': animated | |||
} | |||
}; | |||
if (typeof callback == 'function') | |||
setTimeout(function(){callback(callbackArgs)}, animated); | |||
setTimeout(function(){callback(callbackArgs);}, animated); | |||
} else { | |||
setHfov(hfov); | |||
} | |||
@@ -2796,7 +2822,7 @@ this.lookAt = function(pitch, yaw, hfov, animated, callback, callbackArgs) { | |||
if (typeof callback == 'function') | |||
callback(callbackArgs); | |||
return this; | |||
} | |||
}; | |||
/** | |||
* Returns the panorama's north offset. | |||
@@ -2871,15 +2897,19 @@ this.setHorizonPitch = function(pitch) { | |||
/** | |||
* Start auto rotation. | |||
* | |||
* Before starting rotation, the viewer is panned to `pitch`. | |||
* @memberof Viewer | |||
* @instance | |||
* @param {number} [speed] - Auto rotation speed / direction. If not specified, previous value is used. | |||
* @param {number} [pitch] - The pitch to rotate at. If not specified, inital pitch is used. | |||
* @returns {Viewer} `this` | |||
*/ | |||
this.startAutoRotate = function(speed) { | |||
this.startAutoRotate = function(speed, pitch) { | |||
speed = speed || autoRotateSpeed || 1; | |||
pitch = pitch === undefined ? origPitch : pitch; | |||
config.autoRotate = speed; | |||
_this.lookAt(origPitch, undefined, origHfov, 3000); | |||
_this.lookAt(pitch, undefined, origHfov, 3000); | |||
animateInit(); | |||
return this; | |||
}; | |||
@@ -2905,7 +2935,7 @@ this.stopAutoRotate = function() { | |||
this.stopMovement = function() { | |||
stopAnimation(); | |||
speed = {'yaw': 0, 'pitch': 0, 'hfov': 0}; | |||
} | |||
}; | |||
/** | |||
* Returns the panorama renderer. | |||
@@ -2931,7 +2961,7 @@ this.setUpdate = function(bool) { | |||
else | |||
animateInit(); | |||
return this; | |||
} | |||
}; | |||
/** | |||
* Calculate panorama pitch and yaw from location of mouse event. | |||
@@ -2942,7 +2972,7 @@ this.setUpdate = function(bool) { | |||
*/ | |||
this.mouseEventToCoords = function(event) { | |||
return mouseEventToCoords(event); | |||
} | |||
}; | |||
/** | |||
* Change scene being viewed. | |||
@@ -2958,7 +2988,7 @@ this.loadScene = function(sceneId, pitch, yaw, hfov) { | |||
if (loaded !== false) | |||
loadScene(sceneId, pitch, yaw, hfov); | |||
return this; | |||
} | |||
}; | |||
/** | |||
* Get ID of current scene. | |||
@@ -2968,7 +2998,7 @@ this.loadScene = function(sceneId, pitch, yaw, hfov) { | |||
*/ | |||
this.getScene = function() { | |||
return config.scene; | |||
} | |||
}; | |||
/** | |||
* Add a new scene. | |||
@@ -3006,7 +3036,7 @@ this.removeScene = function(sceneId) { | |||
this.toggleFullscreen = function() { | |||
toggleFullscreen(); | |||
return this; | |||
} | |||
}; | |||
/** | |||
* Get configuration of current scene. | |||
@@ -3016,7 +3046,7 @@ this.toggleFullscreen = function() { | |||
*/ | |||
this.getConfig = function() { | |||
return config; | |||
} | |||
}; | |||
/** | |||
* Get viewer's container element. | |||
@@ -3026,7 +3056,7 @@ this.getConfig = function() { | |||
*/ | |||
this.getContainer = function() { | |||
return container; | |||
} | |||
}; | |||
/** | |||
* Add a new hot spot. | |||
@@ -3052,7 +3082,7 @@ this.addHotSpot = function(hs, sceneId) { | |||
} | |||
initialConfig.scenes[id].hotSpots.push(hs); // Add hot spot to config | |||
} else { | |||
throw 'Invalid scene ID!' | |||
throw 'Invalid scene ID!'; | |||
} | |||
} | |||
if (sceneId === undefined || config.scene == sceneId) { | |||
@@ -3062,7 +3092,7 @@ this.addHotSpot = function(hs, sceneId) { | |||
renderHotSpot(hs); | |||
} | |||
return this; | |||
} | |||
}; | |||
/** | |||
* Remove a hot spot. | |||
@@ -3094,11 +3124,11 @@ this.removeHotSpot = function(hotSpotId, sceneId) { | |||
if (initialConfig.scenes.hasOwnProperty(sceneId)) { | |||
if (!initialConfig.scenes[sceneId].hasOwnProperty('hotSpots')) | |||
return false; | |||
for (var i = 0; i < initialConfig.scenes[sceneId].hotSpots.length; i++) { | |||
if (initialConfig.scenes[sceneId].hotSpots[i].hasOwnProperty('id') && | |||
initialConfig.scenes[sceneId].hotSpots[i].id === hotSpotId) { | |||
for (var j = 0; j < initialConfig.scenes[sceneId].hotSpots.length; j++) { | |||
if (initialConfig.scenes[sceneId].hotSpots[j].hasOwnProperty('id') && | |||
initialConfig.scenes[sceneId].hotSpots[j].id === hotSpotId) { | |||
// Remove hot spot from configuration | |||
initialConfig.scenes[sceneId].hotSpots.splice(i, 1); | |||
initialConfig.scenes[sceneId].hotSpots.splice(j, 1); | |||
return true; | |||
} | |||
} | |||
@@ -3106,7 +3136,7 @@ this.removeHotSpot = function(hotSpotId, sceneId) { | |||
return false; | |||
} | |||
} | |||
} | |||
}; | |||
/** | |||
* This method should be called if the viewer's container is resized. | |||
@@ -3116,7 +3146,7 @@ this.removeHotSpot = function(hotSpotId, sceneId) { | |||
this.resize = function() { | |||
if (renderer) | |||
onDocumentResize(); | |||
} | |||
}; | |||
/** | |||
* Check if a panorama is loaded. | |||
@@ -3126,7 +3156,7 @@ this.resize = function() { | |||
*/ | |||
this.isLoaded = function() { | |||
return loaded; | |||
} | |||
}; | |||
/** | |||
* Check if device orientation control is supported. | |||
@@ -3136,7 +3166,7 @@ this.isLoaded = function() { | |||
*/ | |||
this.isOrientationSupported = function() { | |||
return orientationSupport || false; | |||
} | |||
}; | |||
/** | |||
* Stop using device orientation. | |||
@@ -3145,7 +3175,7 @@ this.isOrientationSupported = function() { | |||
*/ | |||
this.stopOrientation = function() { | |||
stopOrientation(); | |||
} | |||
}; | |||
/** | |||
* Start using device orientation (does nothing if not supported). | |||
@@ -3155,7 +3185,7 @@ this.stopOrientation = function() { | |||
this.startOrientation = function() { | |||
if (orientationSupport) | |||
startOrientation(); | |||
} | |||
}; | |||
/** | |||
* Check if device orientation control is currently activated. | |||
@@ -3165,7 +3195,7 @@ this.startOrientation = function() { | |||
*/ | |||
this.isOrientationActive = function() { | |||
return Boolean(orientation); | |||
} | |||
}; | |||
/** | |||
* Subscribe listener to specified event. | |||
@@ -3179,7 +3209,7 @@ this.on = function(type, listener) { | |||
externalEventListeners[type] = externalEventListeners[type] || []; | |||
externalEventListeners[type].push(listener); | |||
return this; | |||
} | |||
}; | |||
/** | |||
* Remove an event listener (or listeners). | |||
@@ -3209,7 +3239,7 @@ this.off = function(type, listener) { | |||
delete externalEventListeners[type]; | |||
} | |||
return this; | |||
} | |||
}; | |||
/** | |||
* Fire listeners attached to specified event. | |||
@@ -3249,7 +3279,7 @@ this.destroy = function() { | |||
} | |||
container.innerHTML = ''; | |||
container.classList.remove('pnlm-container'); | |||
} | |||
}; | |||
} | |||
@@ -16,7 +16,6 @@ | |||
</noscript> | |||
</div> | |||
<script type="text/javascript" src="../js/libpannellum.js"></script> | |||
<script type="text/javascript" src="../js/RequestAnimationFrame.js"></script> | |||
<script type="text/javascript" src="../js/pannellum.js"></script> | |||
<script type="text/javascript" src="standalone.js"></script> | |||
</body> | |||
@@ -0,0 +1,141 @@ | |||
""" | |||
Selenium-based test suite for Pannellum | |||
Dependencies: | |||
Python 3, Selenium Python bindings, Firefox, geckodriver, Pillow, NumPy | |||
""" | |||
import http.server | |||
import time | |||
import threading | |||
import io | |||
import subprocess | |||
import os | |||
import numpy as np | |||
from PIL import Image, ImageChops | |||
from selenium import webdriver | |||
# Set to true to create a new set of reference images | |||
CREATE_REF = False | |||
# Run web server | |||
print("Starting web server...") | |||
os.chdir(os.path.dirname(os.path.abspath(__file__))) # cd to script dir | |||
os.chdir("..") | |||
httpd = http.server.HTTPServer( | |||
("localhost", 8000), http.server.SimpleHTTPRequestHandler | |||
) | |||
thread = threading.Thread(None, httpd.serve_forever) | |||
thread.start() | |||
# Create a new instance of the Firefox driver | |||
print("Starting web driver...") | |||
if os.environ.get("TRAVIS_JOB_NUMBER"): | |||
# Configuration for Travis CI / Sauce Labs testing | |||
driver = webdriver.Remote( | |||
command_executor="https://ondemand.saucelabs.com:443/wd/hub", | |||
desired_capabilities={ | |||
"username": os.environ["SAUCE_USERNAME"], | |||
"accessKey": os.environ["SAUCE_ACCESS_KEY"], | |||
"tunnel-identifier": os.environ["TRAVIS_JOB_NUMBER"], | |||
"build": os.environ["TRAVIS_JOB_NUMBER"], | |||
"browserName": "firefox", | |||
"seleniumVersion": "3.141.0", | |||
}, | |||
) | |||
else: | |||
fp = webdriver.FirefoxProfile() | |||
fp.set_preference("layout.css.devPixelsPerPx", "1.0") | |||
driver = webdriver.Firefox(firefox_profile=fp) | |||
driver.set_window_size(800, 600) | |||
def run_tests(): | |||
# Load page | |||
print("Loading page...") | |||
driver.get("http://localhost:8000/tests/tests.html") | |||
# Make sure viewer loaded | |||
print("Running tests...") | |||
time.sleep(5) | |||
viewer = driver.find_element_by_id("panorama") | |||
assert driver.execute_script("return viewer.isLoaded()") == True | |||
# Check equirectangular | |||
assert driver.execute_script("return viewer.getScene() == 'equirectangular'") | |||
if CREATE_REF: | |||
viewer.screenshot("tests/equirectangular.png") | |||
subprocess.call(["optipng", "-o7", "-strip", "all", "equirectangular.png"]) | |||
else: | |||
reference = Image.open("tests/equirectangular.png") | |||
screenshot = Image.open(io.BytesIO(viewer.screenshot_as_png)).convert("RGB") | |||
diff = np.mean(np.array(ImageChops.difference(screenshot, reference))) | |||
print("equirectangular difference:", diff) | |||
assert diff < 3 | |||
print("PASS: equirectangular") | |||
# Check movement | |||
driver.execute_script("viewer.setPitch(30).setYaw(-20).setHfov(90)") | |||
time.sleep(2) | |||
assert driver.execute_script( | |||
"return viewer.getPitch() == 30 && viewer.getYaw() == -20 && viewer.getHfov() == 90" | |||
) | |||
driver.find_element_by_class_name("pnlm-zoom-in").click() | |||
time.sleep(1) | |||
assert driver.execute_script("return viewer.getHfov() == 85") | |||
driver.find_element_by_class_name("pnlm-zoom-out").click() | |||
time.sleep(1) | |||
assert driver.execute_script("return viewer.getHfov() == 90") | |||
print("PASS: movement") | |||
# Check look at | |||
driver.execute_script("viewer.lookAt(-10, 90, 100)") | |||
time.sleep(2) | |||
assert driver.execute_script( | |||
"return viewer.getPitch() == -10 && viewer.getYaw() == 90 && viewer.getHfov() == 100" | |||
) | |||
print("PASS: look at") | |||
# Check cube | |||
driver.execute_script("viewer.loadScene('cube')") | |||
time.sleep(5) | |||
assert driver.execute_script("return viewer.getScene() == 'cube'") | |||
if CREATE_REF: | |||
viewer.screenshot("tests/cube.png") | |||
subprocess.call(["optipng", "-o7", "-strip", "all", "cube.png"]) | |||
else: | |||
reference = Image.open("tests/cube.png") | |||
screenshot = Image.open(io.BytesIO(viewer.screenshot_as_png)).convert("RGB") | |||
diff = np.mean(np.array(ImageChops.difference(screenshot, reference))) | |||
print("cube difference:", diff) | |||
assert diff < 3 | |||
print("PASS: cube") | |||
# Check hot spot | |||
driver.find_element_by_class_name("pnlm-scene").click() | |||
time.sleep(5) | |||
assert driver.execute_script("return viewer.getScene() == 'multires'") | |||
print("PASS: hot spot") | |||
# Check multires | |||
if CREATE_REF: | |||
viewer.screenshot("tests/multires.png") | |||
subprocess.call(["optipng", "-o7", "-strip", "all", "multires.png"]) | |||
else: | |||
reference = Image.open("tests/multires.png") | |||
screenshot = Image.open(io.BytesIO(viewer.screenshot_as_png)).convert("RGB") | |||
diff = np.mean(np.array(ImageChops.difference(screenshot, reference))) | |||
print("multires difference:", diff) | |||
assert diff < 3 | |||
print("PASS: multires") | |||
try: | |||
run_tests() | |||
finally: | |||
driver.quit() | |||
httpd.shutdown() | |||
thread.join() |
@@ -0,0 +1,87 @@ | |||
<!DOCTYPE HTML> | |||
<html> | |||
<head> | |||
<meta charset="utf-8"> | |||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |||
<title>Pannellum Tests</title> | |||
<link rel="stylesheet" href="../src/css/pannellum.css"/> | |||
<script type="text/javascript" src="../src/js/libpannellum.js"></script> | |||
<script type="text/javascript" src="../src/js/pannellum.js"></script> | |||
<style> | |||
#panorama { | |||
width: 300px; | |||
height: 200px; | |||
} | |||
</style> | |||
</head> | |||
<body> | |||
<div id="panorama"></div> | |||
<script> | |||
viewer = pannellum.viewer('panorama', { | |||
"default": { | |||
"author": "testauthor", | |||
"firstScene": "equirectangular", | |||
"autoLoad": true | |||
}, | |||
"scenes": { | |||
"cube": { | |||
"title": "cube title", | |||
"type": "cubemap", | |||
"cubeMap": [ | |||
"../examples/multires/1/f00.png", | |||
"../examples/multires/1/r00.png", | |||
"../examples/multires/1/b00.png", | |||
"../examples/multires/1/l00.png", | |||
"../examples/multires/1/u00.png", | |||
"../examples/multires/1/d00.png" | |||
], | |||
"hotSpots": [ | |||
{ | |||
"pitch": -12, | |||
"yaw": 170, | |||
"type": "info", | |||
"text": "info test" | |||
}, | |||
{ | |||
"pitch": -10, | |||
"yaw": -50, | |||
"type": "info", | |||
"text": "link test", | |||
"URL": "https://github.com/mpetroff/pannellum" | |||
}, | |||
{ | |||
"pitch": 0, | |||
"yaw": -10, | |||
"type": "scene", | |||
"text": "scene test", | |||
"sceneId": "multires" | |||
} | |||
] | |||
}, | |||
"equirectangular": { | |||
"title": "equirectangular title", | |||
"panorama": "../examples/examplepano.jpg" | |||
}, | |||
"multires": { | |||
"title": "multires title", | |||
"type": "multires", | |||
"multiRes": { | |||
"basePath": "../examples/multires", | |||
"path": "/%l/%s%x%y", | |||
"fallbackPath": "/fallback/%s", | |||
"extension": "png", | |||
"tileResolution": 256, | |||
"maxLevel": 4, | |||
"cubeResolution": 2048 | |||
} | |||
} | |||
} | |||
}); | |||
</script> | |||
</body> | |||
</html> |
@@ -8,7 +8,6 @@ import urllib.parse | |||
JS = [ | |||
'js/libpannellum.js', | |||
'js/RequestAnimationFrame.js', | |||
'js/pannellum.js', | |||
] | |||
@@ -132,7 +131,6 @@ def build(files, css, html, filename, release=False): | |||
html = merge(html) | |||
html = html.replace('<link type="text/css" rel="Stylesheet" href="../css/pannellum.css"/>','<style type="text/css">' + standalone_css + '</style>') | |||
html = html.replace('<script type="text/javascript" src="../js/libpannellum.js"></script>','') | |||
html = html.replace('<script type="text/javascript" src="../js/RequestAnimationFrame.js"></script>','') | |||
html = html.replace('<script type="text/javascript" src="../js/pannellum.js"></script>','<script type="text/javascript">' + standalone_js + '</script>') | |||
html = html.replace('<script type="text/javascript" src="standalone.js"></script>','') | |||
html = html.replace('<link type="text/css" rel="Stylesheet" href="standalone.css"/>', '') | |||
@@ -143,6 +141,7 @@ def build(files, css, html, filename, release=False): | |||
output(addHeaderJS(js, version), folder + filename) | |||
def main(): | |||
os.chdir(os.path.dirname(os.path.abspath(__file__))) # cd to script dir | |||
if (len(sys.argv) > 1 and sys.argv[1] == 'release'): | |||
build(JS, CSS, HTML, 'pannellum', True) | |||
else: | |||