@@ -0,0 +1,3 @@ | |||
# 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`. Pull requests should preferably be created from [feature branches](https://www.atlassian.com/git/tutorials/comparing-workflows/feature-branch-workflow). |
@@ -0,0 +1 @@ | |||
ko_fi: mpetroff |
@@ -9,3 +9,6 @@ utils/doc/generated_docs | |||
# Ignore IntelliJ Files | |||
.idea | |||
# Ignore logs | |||
tests/*.log |
@@ -0,0 +1,21 @@ | |||
language: generic | |||
dist: xenial | |||
services: | |||
- xvfb | |||
addons: | |||
chrome: stable | |||
before_install: | |||
- sudo apt-get install -y python3-pillow python3-numpy python3-pip mesa-utils libgl1-mesa-dri libglapi-mesa libosmesa6 | |||
- sudo pip3 install selenium | |||
install: | |||
- CHROME_VERSION=`google-chrome --version | sed -r 's/Google Chrome ([0-9]+\.[0-9]+\.[0-9]+).*/\1/'` | |||
- LATEST_CHROMEDRIVER_VERSION=`curl -s "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_${CHROME_VERSION}"` | |||
- curl "https://chromedriver.storage.googleapis.com/${LATEST_CHROMEDRIVER_VERSION}/chromedriver_linux64.zip" -O | |||
- unzip chromedriver_linux64.zip | |||
- sudo mv chromedriver /usr/local/bin | |||
jobs: | |||
include: | |||
- stage: build | |||
script: python3 utils/build/build.py | |||
- stage: test | |||
script: xvfb-run -a 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.3 |
@@ -2,8 +2,92 @@ Changelog | |||
========= | |||
Changes in Pannellum 2.4.1 | |||
-------------------------- | |||
Changes in Pannellum 2.5.3 (2019-08-21) | |||
--------------------------------------- | |||
Bugfixes: | |||
- Fixed loading scenes after previous load failure | |||
- Fixed regression that caused auto-rotation to stop after one revolution | |||
- Fixed behavior of `multiResMinHfov` to match documentation; | |||
default multi-resolution `minHfov` behavior now matches pre-v2.5 | |||
Improvements: | |||
- Added optional `scale` parameter to scale hot spots while zooming | |||
- Improved recovery from failed scene loading | |||
API improvements: | |||
- Added optional pitch argument to `startAutoRotate` | |||
Other: | |||
- Added Journal of Open Source Software (JOSS) manuscript | |||
- Numerous documentation improvements per JOSS review (thanks @vsoch and @Fil) | |||
- Improved continuous integration and automated testing support | |||
- Clarified `generate.py` error messages | |||
- Added Dockerfile for `generate.py` | |||
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 +95,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 +161,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 +183,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 +253,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 +267,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 +335,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 +385,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 +418,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.3", | |||
"bugs": { | |||
"url": "https://github.com/mpetroff/pannellum/issues" | |||
}, | |||
@@ -0,0 +1,109 @@ | |||
@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})}, | |||
} | |||
@online{OSM2018, | |||
author = {{OpenStreetMap}}, | |||
title = {{Bing} {Streetside} imagery now available in {OpenStreetMap} {iD} editor}, | |||
year = {2018}, | |||
month = jul, | |||
url = {https://blog.openstreetmap.org/2018/07/03/bing-streetside-in-id/}, | |||
urldate = {2019-08-06}, | |||
} |
@@ -0,0 +1,71 @@ | |||
--- | |||
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. As an aside, the | |||
name _Pannellum_ was derived as a portmanteau of "panorama" and "vellum," as | |||
this made a unique, pronounceable word, with "vellum" used as a quasi-synonym | |||
to the ``<canvas>`` drawing surface used by the viewer. | |||
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 webpage elements. One such example is integrating the | |||
viewer with a map [@Gede2015; @Albrizio2013; @OSM2018]; the locations where | |||
panoramas were taken can be displayed as markers on the map, whereby clicking a | |||
marker will open the corresponding panorama in the viewer. Panoramic | |||
videos---videos that cover up to a full 360 degrees of azimuth---are supported | |||
via a bundled extension, which is built using | |||
the API. The viewer's underlying rendering code is separate from its 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 on a webpage, 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. [Documentation](https://pannellum.org/documentation/overview/) and | |||
[interactive examples](https://pannellum.org/documentation/examples/simple-example/) | |||
are provided at [pannellum.org](https://pannellum.org/). | |||
# References |
@@ -1,49 +1,98 @@ | |||
# 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) | |||
[![DOI](https://joss.theoj.org/papers/10.21105/joss.01628/status.svg)](https://doi.org/10.21105/joss.01628) | |||
## 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. | |||
## Getting started | |||
### Hosted examples | |||
A set of [examples](https://pannellum.org/documentation/examples/simple-example/) that demonstrate the viewer's various functionality is hosted on [pannellum.org](https://pannellum.org/). This is the best place to start if you want an overview of Pannellum's functionality. They also provide helpful starting points for creating custom configurations. | |||
### Simple tutorial and configuration utility | |||
If you are just looking to display a single panorama without any advanced functionality, the steps for doing so are covered on the [simple tutorial page](https://pannellum.org/documentation/overview/tutorial/). Said page also includes a utility for easily creating the necessary Pannellum configuration. | |||
### Local testing and self-hosting | |||
If you would like to locally test or self-host Pannellum, continue to the _How to use_ section below. | |||
## How to use | |||
1. Upload `build/pannellum.htm` and a full equirectangular panorama to a web server. | |||
* Due to browser security restrictions, a web server must be used locally as well. With Python 2, one can use `python -m SimpleHTTPServer`, and with Python 3, one can use `python -m http.server`, but any other web server will work as well. | |||
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. | |||
1. Upload `build/pannellum.htm` and a full equirectangular panorama to a web server or run a development web server locally. | |||
* Due to browser security restrictions, _a web server must be used locally as well_. With Python 3, one can use `python3 -m http.server`, but any other web server should also work. | |||
2. Use the included multi-resolution generator (`utils/multires/generate.py`), the configuration tool (`utils/config/configuration.htm`), or create a configuration from scratch or based on an [example](https://pannellum.org/documentation/examples/simple-example/). | |||
3. Insert the generated `<iframe>` code into a page, or create a more advanced configuration with [JSON](https://pannellum.org/documentation/reference) or the [API](https://pannellum.org/documentation/api/). | |||
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 a minified copy | |||
For final deployment, it is recommended that one use a minified copy of Pannellum instead of using the source files in `src` directly. The easiest method is to download the most recent [release](https://github.com/mpetroff/pannellum/releases) and use the pre-built copy of either `pannellum.htm` or `pannellum.js` & `pannellum.css`. If you wish to make changes to Pannellum or use the latest development copy of the code, follow the instructions in the _Building_ section below to create `build/pannellum.htm`, `build/pannellum.js`, and `build/pannellum.css`. | |||
### 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 | |||
``` | |||
python generate.py pano_image.jpg | |||
python3 generate.py pano_image.jpg | |||
``` | |||
in the `utils/multires` directory. This will generate all the image tiles and the `config.json` file in the `./output` folder by default. For this to work, `nona` needs to be on the system path; otherwise, the location of `nona` can be specified using the `-n` flag. On a Unix-like platform, with `nona` already on the system path use: | |||
```bash | |||
$ cd utils/multires | |||
$ python3 generate.py pano_image.jpg | |||
``` | |||
where `pano_image.jpg` is the filename of your equirectangular panorama. If `nona` is not on the system path, use: | |||
```bash | |||
$ cd utils/multires | |||
$ python3 generate.py -n /path/to/nona pano_image.jpg | |||
``` | |||
For a complete list of options, run: | |||
```bash | |||
$ python3 generate.py --help | |||
``` | |||
To view the generated configuration, run: | |||
```bash | |||
$ cd ../.. | |||
$ python3 -m http.server | |||
``` | |||
in the `utils/multires` directory. This will generate all the image tiles and the `config.json` file in the `./output` folder by default. For this to work, `nona` needs to be on the system path; otherwise, the location of `nona` can be specified using the `-n` flag, e.g. `python generate.py -n /path/to/nona pano_image.jpg`. | |||
This goes back to the root directory of the repository and starts a local development web server. Then open http://localhost:8000/src/standalone/pannellum.htm?config=../../utils/multires/output/config.json in your web browser of choice. | |||
## Examples | |||
Examples using both the minified version and the version in the `src` directory are included in the `examples` directory. | |||
## Bundled examples | |||
Examples using both the minified version and the version in the `src` directory are included in the `examples` directory. These can be viewed by starting a local web server in the root of the repository, e.g., by running: | |||
```bash | |||
$ python3 -m http.server | |||
``` | |||
in the directory containing this readme file, and then navigating to the hosted HTML files using a web browser; note that the examples use files from the `src` directory, so **the web server must be started from the repository root, not the `examples` directory**. For the `example-minified.htm` example to work, a minified copy of Pannellum must first be built; see the _Building_ section below for details. | |||
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: | |||
@@ -54,7 +103,45 @@ Mobile / app frameworks are not officially supported. They may work, but they're | |||
All user-facing strings can be changed using the `strings` configuration parameter. There exists a [third-party respository of user-contributed translations](https://github.com/DanielBiegler/pannellum-translation) that can be used with this configuration option. | |||
## 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. | |||
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. On a Unix-like platform: | |||
```bash | |||
$ cd utils/build | |||
$ ./build.sh | |||
``` | |||
If successful, this should create `build/pannellum.htm`, `build/pannellum.js`, and `build/pannellum.css`, relative to the root directory of the repository. | |||
## Tests | |||
A minimal [Selenium](https://www.seleniumhq.org/)-based test suite is located in the `tests` directory. The tests can be executed by running: | |||
```bash | |||
python3 run_tests.py | |||
``` | |||
A Selenium-driven web browser (with a Chrome driver, by default) is created, and screenshots are generated | |||
and compared against previously generated ones in [tests](tests). For example, to regenerate the screenshots | |||
one can run: | |||
```bash | |||
$ python3 tests/run_tests.py --create-ref | |||
``` | |||
And to simply run the tests to compare to, eliminate that argument. By default, a random | |||
port is selected, along with other arguments. One can see usage via: | |||
```bash | |||
$ python tests/run_tests.py --help | |||
``` | |||
Continuous integration tests are run via [Travis CI](https://travis-ci.org/mpetroff/pannellum). Running the tests locally requires Python 3, the Selenium Python bindings, [Pillow](https://pillow.readthedocs.io/), [NumPy](https://www.numpy.org/), and either Firefox & [geckodriver](https://github.com/mozilla/geckodriver) or Chrome & [ChromeDriver](https://chromedriver.chromium.org/). | |||
## Seeking support | |||
If you wish to ask a question or report a bug, please open an issue at [github.com/mpetroff/pannellum](https://github.com/mpetroff/pannellum). See the _Contributing_ section below for more details. | |||
## 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`. Pull requests should preferably be created from [feature branches](https://www.atlassian.com/git/tutorials/comparing-workflows/feature-branch-workflow). | |||
## 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). | |||
@@ -67,3 +154,7 @@ The panoramic image provided with the examples is licensed under the [Creative C | |||
* [Matthew Petroff](http://mpetroff.net/), Original Author | |||
* [three.js](https://github.com/mrdoob/three.js) r40, Former Underlying Framework | |||
If used as part of academic research, please cite: | |||
> Petroff, Matthew A. "Pannellum: a lightweight web-based panorama viewer." _Journal of Open Source Software_ 4, no. 40 (2019): 1628. [doi:10.21105/joss.01628](https://doi.org/10.21105/joss.01628) |
@@ -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 | |||
@@ -266,9 +266,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); | |||
@@ -304,9 +304,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}; | |||
} | |||
} | |||
@@ -695,9 +695,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 = []; | |||
@@ -1153,7 +1153,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(src, texture, callback) { | |||
this.texture = texture; | |||
@@ -1166,7 +1166,7 @@ function Renderer(container) { | |||
this.src = src; | |||
this.texture = texture; | |||
this.callback = callback; | |||
}; | |||
} | |||
function releaseTextureImageLoader(til) { | |||
if (pendingTextureRequests.length) { | |||
@@ -1196,7 +1196,7 @@ function Renderer(container) { | |||
* @param {MultiresNode} node - Input node. | |||
*/ | |||
function processNextTile(node) { | |||
loadTexture(node, encodeURI(node.path + '.' + image.extension), function(texture, loaded) { | |||
loadTexture(node, node.path + '.' + image.extension, function(texture, loaded) { | |||
node.texture = texture; | |||
node.textureLoaded = loaded ? 2 : 1; | |||
}, globalParams.crossOrigin); | |||
@@ -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 | |||
@@ -68,7 +68,8 @@ var config, | |||
specifiedPhotoSphereExcludes = [], | |||
update = false, // Should we update when still to render dynamic content | |||
eps = 1e-6, | |||
hotspotsCreated = false; | |||
hotspotsCreated = false, | |||
destroyed = false; | |||
var defaultConfig = { | |||
hfov: 100, | |||
@@ -134,7 +135,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; | |||
@@ -446,6 +447,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(); | |||
} | |||
} | |||
/** | |||
@@ -457,7 +465,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. | |||
@@ -617,6 +625,7 @@ function anError(errorMsg) { | |||
infoDisplay.load.box.style.display = 'none'; | |||
infoDisplay.errorMsg.style.display = 'table'; | |||
error = true; | |||
loaded = undefined; | |||
renderContainer.style.display = 'none'; | |||
fireEvent('error', errorMsg); | |||
} | |||
@@ -630,6 +639,7 @@ function clearError() { | |||
infoDisplay.load.box.style.display = 'none'; | |||
infoDisplay.errorMsg.style.display = 'none'; | |||
error = false; | |||
renderContainer.style.display = 'block'; | |||
fireEvent('errorcleared'); | |||
} | |||
} | |||
@@ -1211,12 +1221,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; | |||
} | |||
@@ -1349,6 +1358,10 @@ function animateInit() { | |||
* @private | |||
*/ | |||
function animate() { | |||
if (destroyed) { | |||
return; | |||
} | |||
render(); | |||
if (autoRotateStart) | |||
clearTimeout(autoRotateStart); | |||
@@ -1399,6 +1412,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; | |||
@@ -1406,8 +1430,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) { | |||
@@ -1433,10 +1456,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 | |||
@@ -1447,7 +1474,6 @@ function render() { | |||
} | |||
// Ensure the calculated pitch is within min and max allowed | |||
var canvas = renderer.getCanvas(); | |||
var vfov = 2 * Math.atan(Math.tan(config.hfov / 180 * Math.PI * 0.5) / | |||
(canvas.width / canvas.height)) / Math.PI * 180; | |||
var minPitch = config.minPitch + vfov / 2, | |||
@@ -1502,7 +1528,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. | |||
@@ -1516,7 +1542,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. | |||
@@ -1564,6 +1590,8 @@ function computeQuaternion(alpha, beta, gamma) { | |||
* @param {DeviceOrientationEvent} event - Device orientation event. | |||
*/ | |||
function orientationListener(e) { | |||
if (e.hasOwnProperty('requestPermission')) | |||
e.requestPermission() | |||
var q = computeQuaternion(e.alpha, e.beta, e.gamma).toEulerAngles(); | |||
if (typeof(orientation) == 'number' && orientation < 10) { | |||
// This kludge is necessary because iOS sometimes provides a few stale | |||
@@ -1652,10 +1680,10 @@ function renderInitCallback() { | |||
preview = undefined; | |||
} | |||
loaded = true; | |||
fireEvent('load'); | |||
animateInit(); | |||
fireEvent('load'); | |||
} | |||
/** | |||
@@ -1669,7 +1697,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 | |||
@@ -1682,24 +1710,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); | |||
@@ -1751,7 +1779,7 @@ function createHotSpot(hs) { | |||
span.className += ' pnlm-pointer'; | |||
} | |||
hs.div = div; | |||
}; | |||
} | |||
/** | |||
* Creates hot spot elements for the current scene. | |||
@@ -1831,6 +1859,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; | |||
@@ -1975,7 +2006,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; | |||
@@ -1985,7 +2024,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 | |||
@@ -2154,12 +2193,12 @@ function zoomOut() { | |||
function constrainHfov(hfov) { | |||
// Keep field of view within bounds | |||
var minHfov = config.minHfov; | |||
if (config.type == 'multires' && renderer && config.multiResMinHfov) { | |||
if (config.type == 'multires' && renderer && !config.multiResMinHfov) { | |||
minHfov = Math.min(minHfov, renderer.getCanvas().width / (config.multiRes.cubeResolution / 90 * 0.9)); | |||
} | |||
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; | |||
@@ -2175,8 +2214,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; | |||
} | |||
@@ -2227,6 +2265,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 = {}; | |||
@@ -2291,13 +2331,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(); | |||
} | |||
} | |||
/** | |||
@@ -2410,9 +2443,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; | |||
} | |||
@@ -2471,22 +2504,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; | |||
} | |||
@@ -2551,9 +2584,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); | |||
} | |||
@@ -2614,7 +2647,7 @@ this.lookAt = function(pitch, yaw, hfov, animated, callback, callbackArgs) { | |||
if (typeof callback == 'function') | |||
callback(callbackArgs); | |||
return this; | |||
} | |||
}; | |||
/** | |||
* Returns the panorama's north offset. | |||
@@ -2689,15 +2722,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; | |||
}; | |||
@@ -2723,7 +2760,7 @@ this.stopAutoRotate = function() { | |||
this.stopMovement = function() { | |||
stopAnimation(); | |||
speed = {'yaw': 0, 'pitch': 0, 'hfov': 0}; | |||
} | |||
}; | |||
/** | |||
* Returns the panorama renderer. | |||
@@ -2749,7 +2786,7 @@ this.setUpdate = function(bool) { | |||
else | |||
animateInit(); | |||
return this; | |||
} | |||
}; | |||
/** | |||
* Calculate panorama pitch and yaw from location of mouse event. | |||
@@ -2760,7 +2797,7 @@ this.setUpdate = function(bool) { | |||
*/ | |||
this.mouseEventToCoords = function(event) { | |||
return mouseEventToCoords(event); | |||
} | |||
}; | |||
/** | |||
* Change scene being viewed. | |||
@@ -2776,7 +2813,7 @@ this.loadScene = function(sceneId, pitch, yaw, hfov) { | |||
if (loaded !== false) | |||
loadScene(sceneId, pitch, yaw, hfov); | |||
return this; | |||
} | |||
}; | |||
/** | |||
* Get ID of current scene. | |||
@@ -2786,7 +2823,7 @@ this.loadScene = function(sceneId, pitch, yaw, hfov) { | |||
*/ | |||
this.getScene = function() { | |||
return config.scene; | |||
} | |||
}; | |||
/** | |||
* Add a new scene. | |||
@@ -2824,7 +2861,7 @@ this.removeScene = function(sceneId) { | |||
this.toggleFullscreen = function() { | |||
toggleFullscreen(); | |||
return this; | |||
} | |||
}; | |||
/** | |||
* Get configuration of current scene. | |||
@@ -2834,7 +2871,7 @@ this.toggleFullscreen = function() { | |||
*/ | |||
this.getConfig = function() { | |||
return config; | |||
} | |||
}; | |||
/** | |||
* Get viewer's container element. | |||
@@ -2844,7 +2881,7 @@ this.getConfig = function() { | |||
*/ | |||
this.getContainer = function() { | |||
return container; | |||
} | |||
}; | |||
/** | |||
* Add a new hot spot. | |||
@@ -2870,7 +2907,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) { | |||
@@ -2880,7 +2917,7 @@ this.addHotSpot = function(hs, sceneId) { | |||
renderHotSpot(hs); | |||
} | |||
return this; | |||
} | |||
}; | |||
/** | |||
* Remove a hot spot. | |||
@@ -2912,11 +2949,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; | |||
} | |||
} | |||
@@ -2924,7 +2961,7 @@ this.removeHotSpot = function(hotSpotId, sceneId) { | |||
return false; | |||
} | |||
} | |||
} | |||
}; | |||
/** | |||
* This method should be called if the viewer's container is resized. | |||
@@ -2934,7 +2971,7 @@ this.removeHotSpot = function(hotSpotId, sceneId) { | |||
this.resize = function() { | |||
if (renderer) | |||
onDocumentResize(); | |||
} | |||
}; | |||
/** | |||
* Check if a panorama is loaded. | |||
@@ -2944,7 +2981,7 @@ this.resize = function() { | |||
*/ | |||
this.isLoaded = function() { | |||
return loaded; | |||
} | |||
}; | |||
/** | |||
* Check if device orientation control is supported. | |||
@@ -2954,7 +2991,7 @@ this.isLoaded = function() { | |||
*/ | |||
this.isOrientationSupported = function() { | |||
return orientationSupport || false; | |||
} | |||
}; | |||
/** | |||
* Stop using device orientation. | |||
@@ -2963,7 +3000,7 @@ this.isOrientationSupported = function() { | |||
*/ | |||
this.stopOrientation = function() { | |||
stopOrientation(); | |||
} | |||
}; | |||
/** | |||
* Start using device orientation (does nothing if not supported). | |||
@@ -2973,7 +3010,7 @@ this.stopOrientation = function() { | |||
this.startOrientation = function() { | |||
if (orientationSupport) | |||
startOrientation(); | |||
} | |||
}; | |||
/** | |||
* Check if device orientation control is currently activated. | |||
@@ -2983,7 +3020,7 @@ this.startOrientation = function() { | |||
*/ | |||
this.isOrientationActive = function() { | |||
return Boolean(orientation); | |||
} | |||
}; | |||
/** | |||
* Subscribe listener to specified event. | |||
@@ -2997,7 +3034,7 @@ this.on = function(type, listener) { | |||
externalEventListeners[type] = externalEventListeners[type] || []; | |||
externalEventListeners[type].push(listener); | |||
return this; | |||
} | |||
}; | |||
/** | |||
* Remove an event listener (or listeners). | |||
@@ -3027,7 +3064,7 @@ this.off = function(type, listener) { | |||
delete externalEventListeners[type]; | |||
} | |||
return this; | |||
} | |||
}; | |||
/** | |||
* Fire listeners attached to specified event. | |||
@@ -3049,6 +3086,9 @@ function fireEvent(type) { | |||
* @memberof Viewer | |||
*/ | |||
this.destroy = function() { | |||
destroyed = true; | |||
clearTimeout(autoRotateStart); | |||
if (renderer) | |||
renderer.destroy(); | |||
if (listenersAdded) { | |||
@@ -3067,7 +3107,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,290 @@ | |||
#!/usr/bin/env python3 | |||
""" | |||
Selenium-based test suite for Pannellum | |||
Dependencies: | |||
Python 3, Selenium Python bindings, Pillow, NumPy | |||
Either: Firefox & geckodriver or Chrome & chromedriver | |||
Run tests for Pannellum, set up with Continuous Integration. | |||
Contributed by Vanessa Sochat, JOSS Review 2019. | |||
See the project repository for licensing information. | |||
""" | |||
from random import choice | |||
from threading import Thread | |||
from http.server import SimpleHTTPRequestHandler | |||
from socketserver import TCPServer | |||
import argparse | |||
import io | |||
import os | |||
import re | |||
import subprocess | |||
import sys | |||
import time | |||
import numpy as np | |||
from PIL import Image, ImageChops | |||
from selenium.common.exceptions import TimeoutException | |||
from selenium import webdriver | |||
class PannellumServer(SimpleHTTPRequestHandler): | |||
"""Here we subclass SimpleHTTPServer to capture error messages. | |||
""" | |||
def log_message(self, format, *args): | |||
""" | |||
Log to standard error with a date time string, | |||
and then call any subclass specific logging functions. | |||
""" | |||
sys.stderr.write( | |||
"%s - - [%s] %s\n" | |||
% (self.address_string(), self.log_date_time_string(), format % args) | |||
) | |||
# Workaround for error trying to GET html | |||
if not re.search("div", format % args) and not re.search( | |||
"function", format % args | |||
): | |||
if re.search("404", format % args): | |||
raise IOError(format % args) | |||
def log_error(self, format, *args): | |||
"""Catch errors in the log_messages instead. | |||
""" | |||
pass | |||
class PannellumTester(object): | |||
"""Bring up a server with a testing robot. | |||
""" | |||
def __init__(self, port=None, browser="Chrome", headless=False): | |||
self.handler = PannellumServer | |||
if port: | |||
self.port = port | |||
else: | |||
self.port = choice(range(8000, 9999)) | |||
print("Selected port is %s" % self.port) | |||
self.httpd = TCPServer(("", self.port), self.handler) | |||
self.server = Thread(target=self.httpd.serve_forever) | |||
self.server.setDaemon(True) | |||
self.server.start() | |||
self.started = True | |||
self.pause_time = 100 | |||
self.browser = None | |||
self.headless = headless | |||
self.display = None | |||
self.driver = browser | |||
def take_screenshot(self, element_id, filename=None): | |||
"""Take a screenshot of an element with a given ID. | |||
""" | |||
element = self.browser.find_element_by_id(element_id) | |||
img = Image.open(io.BytesIO(element.screenshot_as_png)).convert("RGB") | |||
if filename is not None: | |||
img.save(filename) | |||
return img | |||
def equal_images(self, reference, comparator, name, threshold=5): | |||
"""Compare two images, both loaded with PIL, based on pixel differences.""" | |||
diff = np.mean(np.array(ImageChops.difference(reference, comparator))) | |||
print("%s difference: %s" % (name, diff)) | |||
if diff >= threshold: | |||
comparator.save("tests/" + name + "-comparison.png") | |||
raise ValueError("Screenshot difference is above threshold!") | |||
def run_tests(self, create_ref=False): | |||
"""Run tests for Pannellum.""" | |||
print("Loading page...") | |||
self.get_page("http://localhost:%s/tests/tests.html" % self.port) | |||
print("Running tests...") | |||
time.sleep(5) | |||
assert self.browser.execute_script("return viewer.isLoaded()") is True | |||
# Check equirectangular | |||
assert self.browser.execute_script( | |||
"return viewer.getScene() == 'equirectangular'" | |||
) | |||
if create_ref: | |||
self.take_screenshot("panorama", "tests/equirectangular.png") | |||
subprocess.call( | |||
["optipng", "-o7", "-strip", "all", "tests/equirectangular.png"] | |||
) | |||
else: | |||
reference = Image.open("tests/equirectangular.png") | |||
comparator = self.take_screenshot("panorama") | |||
self.equal_images(reference, comparator, "equirectangular") | |||
print("PASS: equirectangular") | |||
# Check movement | |||
self.browser.execute_script("viewer.setPitch(30).setYaw(-20).setHfov(90)") | |||
time.sleep(2) | |||
assert self.browser.execute_script( | |||
"return viewer.getPitch() == 30 && viewer.getYaw() == -20 && viewer.getHfov() == 90" | |||
) | |||
self.browser.find_element_by_class_name("pnlm-zoom-in").click() | |||
time.sleep(1) | |||
assert self.browser.execute_script("return viewer.getHfov() == 85") | |||
self.browser.find_element_by_class_name("pnlm-zoom-out").click() | |||
time.sleep(1) | |||
assert self.browser.execute_script("return viewer.getHfov() == 90") | |||
print("PASS: movement") | |||
# Check look at | |||
self.browser.execute_script("viewer.lookAt(-10, 90, 100)") | |||
time.sleep(2) | |||
assert self.browser.execute_script( | |||
"return viewer.getPitch() == -10 && viewer.getYaw() == 90 && viewer.getHfov() == 100" | |||
) | |||
print("PASS: look at") | |||
# Check cube | |||
self.browser.execute_script("viewer.loadScene('cube')") | |||
time.sleep(5) | |||
assert self.browser.execute_script("return viewer.getScene() == 'cube'") | |||
if create_ref: | |||
self.take_screenshot("panorama", "tests/cube.png") | |||
subprocess.call(["optipng", "-o7", "-strip", "all", "tests/cube.png"]) | |||
else: | |||
reference = Image.open("tests/cube.png") | |||
comparator = self.take_screenshot("panorama") | |||
self.equal_images(reference, comparator, "cube") | |||
# Check hot spot | |||
self.browser.find_element_by_class_name("pnlm-scene").click() | |||
time.sleep(5) | |||
assert self.browser.execute_script("return viewer.getScene() == 'multires'") | |||
print("PASS: hot spot") | |||
# Check multires | |||
if create_ref: | |||
self.take_screenshot("panorama", "tests/multires.png") | |||
subprocess.call(["optipng", "-o7", "-strip", "all", "tests/multires.png"]) | |||
else: | |||
reference = Image.open("tests/multires.png") | |||
comparator = self.take_screenshot("panorama") | |||
self.equal_images(reference, comparator, "multires") | |||
self.httpd.server_close() | |||
def get_browser(self, name=None): | |||
"""Return a browser if it hasn't been initialized yet. | |||
""" | |||
if name is None: | |||
name = self.driver | |||
log_path = "tests/%s-driver.log" % name.lower() | |||
if self.browser is None: | |||
if name.lower() == "firefox": | |||
fp = webdriver.FirefoxProfile() | |||
fp.set_preference("layout.css.devPixelsPerPx", "1.0") | |||
self.browser = webdriver.Firefox( | |||
service_log_path=log_path, firefox_profile=fp | |||
) | |||
self.browser.set_window_size(800, 600) | |||
else: | |||
options = webdriver.ChromeOptions() | |||
options.add_argument("headless") | |||
options.add_argument("no-sandbox") | |||
options.add_argument("window-size=800x600") | |||
self.browser = webdriver.Chrome( | |||
service_log_path=log_path, options=options | |||
) | |||
return self.browser | |||
def get_page(self, url): | |||
"""Open a particular URL, checking for timeout. | |||
""" | |||
if self.browser is None: | |||
self.browser = self.get_browser() | |||
try: | |||
return self.browser.get(url) | |||
except TimeoutException: | |||
print("Browser request timeout. Are you connected to the internet?") | |||
self.browser.close() | |||
sys.exit(1) | |||
def stop(self): | |||
"""Close any running browser or server and shut down the robot. | |||
""" | |||
if self.browser is not None: | |||
self.browser.close() | |||
self.httpd.server_close() | |||
if self.display is not None: | |||
self.display.close() | |||
def get_parser(): | |||
parser = argparse.ArgumentParser(description="Run tests for Pannellum") | |||
parser.add_argument( | |||
"--port", | |||
"-p", | |||
dest="port", | |||
help="Port to run web server", | |||
type=int, | |||
default=None, | |||
) | |||
parser.add_argument( | |||
"--headless", | |||
dest="headless", | |||
help="Start a display before browser", | |||
action="store_true", | |||
default=False, | |||
) | |||
parser.add_argument( | |||
"--create-ref", dest="create_ref", action="store_true", default=False | |||
) | |||
parser.add_argument( | |||
"--browser", | |||
"-b", | |||
dest="browser", | |||
choices=["Firefox", "Chrome"], | |||
help="Browser driver to use for the robot", | |||
type=str, | |||
default="Chrome", | |||
) | |||
return parser | |||
def main(): | |||
parser = get_parser() | |||
try: | |||
args = parser.parse_args() | |||
except: | |||
sys.exit(0) | |||
# Add this script's directory, in case it contains driver binaries | |||
here = os.path.abspath(os.path.dirname(__file__)) | |||
os.environ["PATH"] = here + ":" + os.environ["PATH"] | |||
os.chdir(here) | |||
# We must be in root directory | |||
os.chdir("..") | |||
# Initialize the tester | |||
tester = PannellumTester( | |||
browser=args.browser, port=args.port, headless=args.headless | |||
) | |||
# Run tests | |||
tester.run_tests(create_ref=args.create_ref) | |||
# Clean up shop! | |||
tester.stop() | |||
if __name__ == "__main__": | |||
main() |
@@ -0,0 +1,88 @@ | |||
<!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", | |||
"hfov": 85, | |||
"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', | |||
] | |||
@@ -100,7 +99,11 @@ def build(files, css, html, filename, release=False): | |||
if release: | |||
version = read('../VERSION').strip() | |||
else: | |||
version = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode('utf-8').strip() | |||
if os.path.exists('../../.git'): | |||
version = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode('utf-8').strip() | |||
else: | |||
print('No .git folder detected, setting version to testing') | |||
version = "testing" | |||
js = js.replace('"_blank">Pannellum</a>','"_blank">Pannellum</a> ' + version) | |||
with open('../../src/standalone/standalone.js', 'r') as f: | |||
standalone_js = f.read() | |||
@@ -132,7 +135,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 +145,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: | |||
@@ -1,7 +1,7 @@ | |||
#!/bin/sh | |||
# Generates API documentation. | |||
# Requires documentationjs <http://documentation.js.org/>. | |||
# Requires documentationjs <https://documentation.js.org/>. | |||
# Usage: | |||
# | |||
@@ -19,4 +19,4 @@ elif [ "$1" = "private" ]; then | |||
fi | |||
echo "Generating documentation..." | |||
documentation ../../src/js/pannellum.js ../../src/js/libpannellum.js -o generated_docs -f html --name Pannellum --project-version $version -g $private | |||
documentation build ../../src/js/pannellum.js ../../src/js/libpannellum.js -o generated_docs -f html --project-name Pannellum --project-version $version -g $private |
@@ -0,0 +1,11 @@ | |||
FROM ubuntu:18.04 | |||
# docker build -t generate-panorama . | |||
# docker run -it -v $PWD:/data generate-panorama /data/image.jpg | |||
ENV DEBIAN_FRONTEND noninteractive | |||
RUN apt-get update && \ | |||
apt-get install -y python3 python3-dev python3-pil hugin-tools | |||
ADD generate.py /generate.py | |||
ENTRYPOINT ["python3", "/generate.py"] |
@@ -47,9 +47,17 @@ except KeyError: | |||
# Handle case of PATH not being set | |||
nona = None | |||
# Subclass parser to add explaination for semi-option nona flag | |||
class GenParser(argparse.ArgumentParser): | |||
def error(self, message): | |||
if '--nona' in message: | |||
sys.stderr.write('''IMPORTANT: The location of the nona utility (from Hugin) must be specified | |||
with -n, since it was not found on the PATH!\n\n''') | |||
super(GenParser, self).error(message) | |||
# Parse input | |||
parser = argparse.ArgumentParser(description='Generate a Pannellum multires tile set from a full or partial equirectangular or cylindrical panorama.', | |||
formatter_class=argparse.ArgumentDefaultsHelpFormatter) | |||
parser = GenParser(description='Generate a Pannellum multires tile set from a full or partial equirectangular or cylindrical panorama.', | |||
formatter_class=argparse.ArgumentDefaultsHelpFormatter) | |||
parser.add_argument('inputFile', metavar='INPUT', | |||
help='panorama to be processed') | |||
parser.add_argument('-C', '--cylindrical', action='store_true', | |||
@@ -0,0 +1,105 @@ | |||
# Generating multi-resolution tiles for a panorama | |||
## Get a Panorama | |||
If you don't have your own, it's easy to use one of the examples in the repository. | |||
From this directory, run: | |||
```bash | |||
$ cp ../../examples/examplepano.jpg . | |||
``` | |||
## Generate tiles | |||
To use the `generate.py` script, either its dependencies need to be installed, | |||
or [Docker](https://www.docker.com/) can be used to avoid this installation. | |||
### Option 1: with local dependencies | |||
The `generate.py` script depends on `nona` (from [Hugin](http://hugin.sourceforge.net/)), | |||
as well as Python with the [Pillow](https://pillow.readthedocs.org/) package. On Ubuntu, | |||
these dependencies can be installed by running: | |||
```bash | |||
$ sudo apt install python3 python3-pil hugin-tools | |||
``` | |||
Once the dependencies are installed, a tileset can generated with: | |||
```bash | |||
$ python3 generate.py examplepano.jpg | |||
Processing input image information... | |||
Assuming --haov 360.0 | |||
Assuming --vaov 180.0 | |||
Generating cube faces... | |||
Generating tiles... | |||
Generating fallback tiles... | |||
``` | |||
### Option 2: with Docker | |||
A small Dockerfile is provided that allows one to easily generate a panorama tileset | |||
with the [generate.py](generate.py) script, without needing to install dependencies | |||
on one's host. | |||
First, build the Docker container: | |||
```bash | |||
$ docker build -t generate-panorama . | |||
``` | |||
When it's finished, you can bind the present working directory to a location in | |||
the container (`/data`) so that your image is found in the container. Notice | |||
that the output needs to be specified in a directory that is bound to the host: | |||
```bash | |||
$ docker run -it -v $PWD:/data generate-panorama --output /data/output /data/examplepano.jpg | |||
Processing input image information... | |||
Assuming --haov 360.0 | |||
Assuming --vaov 180.0 | |||
Generating cube faces... | |||
Generating tiles... | |||
Generating fallback tiles... | |||
``` | |||
## Viewing output (for either method) | |||
The final output will be in your present working directory: | |||
```bash | |||
$ ls output/ | |||
1 2 3 config.json fallback | |||
``` | |||
Next, change back to the root and start a server: | |||
```bash | |||
$ cd ../.. | |||
$ python3 -m http.server | |||
``` | |||
A generated tileset and configuration in `utils/multires/output` can then be viewed by navigating a browser to: | |||
[http://localhost:8000/src/standalone/pannellum.htm?config=../../utils/multires/output/config.json](http://localhost:8000/src/standalone/pannellum.htm?config=../../utils/multires/output/config.json) | |||
When the page is loaded, the console will output a logging stream corresponding to the HTTP requests: | |||
```bash | |||
127.0.0.1 - - [09/Aug/2019 09:41:24] "GET /src/standalone/pannellum.htm?config=../../utils/multires/output/config.json HTTP/1.1" 200 - | |||
127.0.0.1 - - [09/Aug/2019 09:41:24] "GET /src/css/pannellum.css HTTP/1.1" 200 - | |||
127.0.0.1 - - [09/Aug/2019 09:41:24] "GET /src/standalone/standalone.css HTTP/1.1" 200 - | |||
127.0.0.1 - - [09/Aug/2019 09:41:24] "GET /src/js/libpannellum.js HTTP/1.1" 200 - | |||
127.0.0.1 - - [09/Aug/2019 09:41:24] "GET /src/js/pannellum.js HTTP/1.1" 200 - | |||
127.0.0.1 - - [09/Aug/2019 09:41:24] "GET /src/standalone/standalone.js HTTP/1.1" 200 - | |||
127.0.0.1 - - [09/Aug/2019 09:41:24] "GET /utils/multires/output/config.json HTTP/1.1" 200 - | |||
127.0.0.1 - - [09/Aug/2019 09:41:24] "GET /src/css/img/background.svg HTTP/1.1" 200 - | |||
127.0.0.1 - - [09/Aug/2019 09:41:24] "GET /src/css/img/sprites.svg HTTP/1.1" 200 - | |||
127.0.0.1 - - [09/Aug/2019 09:41:24] "GET /src/css/img/compass.svg HTTP/1.1" 200 - | |||
127.0.0.1 - - [09/Aug/2019 09:41:26] "GET /src/css/img/grab.svg HTTP/1.1" 200 - | |||
127.0.0.1 - - [09/Aug/2019 09:41:27] "GET /utils/multires/output//1/r0_0.jpg HTTP/1.1" 200 - | |||
127.0.0.1 - - [09/Aug/2019 09:41:27] "GET /utils/multires/output//1/f0_0.jpg HTTP/1.1" 200 - | |||
127.0.0.1 - - [09/Aug/2019 09:41:27] "GET /utils/multires/output//1/u0_0.jpg HTTP/1.1" 200 - | |||
... | |||
``` | |||
The panorama, in multi-resolution format, should display in the browser. |