photo-sphere-viewer.js 88 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045
  1. /*
  2. * Photo Sphere Viewer v2.9
  3. * http://jeremyheleine.me/photo-sphere-viewer
  4. *
  5. * Copyright (c) 2014,2015 Jérémy Heleine
  6. *
  7. * Permission is hereby granted, free of charge, to any person obtaining a copy
  8. * of this software and associated documentation files (the "Software"), to deal
  9. * in the Software without restriction, including without limitation the rights
  10. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. * copies of the Software, and to permit persons to whom the Software is
  12. * furnished to do so, subject to the following conditions:
  13. *
  14. * The above copyright notice and this permission notice shall be included in
  15. * all copies or substantial portions of the Software.
  16. *
  17. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23. * THE SOFTWARE.
  24. */
  25. /**
  26. * Represents a panorama viewer.
  27. * @class
  28. * @param {object} args - Settings to apply to the viewer
  29. * @param {string} args.panorama - Panorama URL or path (absolute or relative)
  30. * @param {HTMLElement|string} args.container - Panorama container (should be a `div` or equivalent), can be a string (the ID of the element to retrieve)
  31. * @param {object} args.overlay - Image to add over the panorama
  32. * @param {string} args.overlay.image - Image URL or path
  33. * @param {object} [args.overlay.position=null] - Image position (default to the bottom left corner)
  34. * @param {string} [args.overlay.position.x=null] - Horizontal image position ('left' or 'right')
  35. * @param {string} [args.overlay.position.y=null] - Vertical image position ('top' or 'bottom')
  36. * @param {object} [args.overlay.size=null] - Image size (if it needs to be resized)
  37. * @param {number|string} [args.overlay.size.width=null] - Image width (in pixels or a percentage, like '20%')
  38. * @param {number|string} [args.overlay.size.height=null] - Image height (in pixels or a percentage, like '20%')
  39. * @param {integer} [args.segments=100] - Number of segments on the sphere
  40. * @param {integer} [args.rings=100] - Number of rings on the sphere
  41. * @param {boolean} [args.autoload=true] - `true` to automatically load the panorama, `false` to load it later (with the {@link PhotoSphereViewer#load|`.load`} method)
  42. * @param {boolean} [args.usexmpdata=true] - `true` if Photo Sphere Viewer must read XMP data, `false` if it is not necessary
  43. * @param {boolean} [args.cors_anonymous=true] - `true` to disable the exchange of user credentials via cookies, `false` otherwise
  44. * @param {object} [args.pano_size=null] - The panorama size, if cropped (unnecessary if XMP data can be read)
  45. * @param {number} [args.pano_size.full_width=null] - The full panorama width, before crop (the image width if `null`)
  46. * @param {number} [args.pano_size.full_height=null] - The full panorama height, before crop (the image height if `null`)
  47. * @param {number} [args.pano_size.cropped_width=null] - The cropped panorama width (the image width if `null`)
  48. * @param {number} [args.pano_size.cropped_height=null] - The cropped panorama height (the image height if `null`)
  49. * @param {number} [args.pano_size.cropped_x=null] - The cropped panorama horizontal offset relative to the full width (middle if `null`)
  50. * @param {number} [args.pano_size.cropped_y=null] - The cropped panorama vertical offset relative to the full height (middle if `null`)
  51. * @param {object} [args.captured_view=null] - The real captured view, compared to the theoritical 360°×180° possible view
  52. * @param {number} [args.captured_view.horizontal_fov=360] - The horizontal captured field of view in degrees (default to 360°)
  53. * @param {number} [args.captured_view.vertical_fov=180] - The vertical captured field of view in degrees (default to 180°)
  54. * @param {object} [args.default_position] - Defines the default position (the first point seen by the user)
  55. * @param {number|string} [args.default_position.long=0] - Default longitude, in radians (or in degrees if indicated, e.g. `'45deg'`)
  56. * @param {number|string} [args.default_position.lat=0] - Default latitude, in radians (or in degrees if indicated, e.g. `'45deg'`)
  57. * @param {number} [args.min_fov=30] - The minimal field of view, in degrees, between 1 and 179
  58. * @param {number} [args.max_fov=90] - The maximal field of view, in degrees, between 1 and 179
  59. * @param {boolean} [args.allow_user_interactions=true] - If set to `false`, the user won't be able to interact with the panorama (navigation bar is then disabled)
  60. * @param {boolean} [args.allow_scroll_to_zoom=true] - It set to `false`, the user won't be able to scroll with their mouse to zoom
  61. * @param {number} [args.zoom_speed=1] - Indicate a number greater than 1 to increase the zoom speed
  62. * @param {number|string} [args.tilt_up_max=π/2] - The maximal tilt up angle, in radians (or in degrees if indicated, e.g. `'30deg'`)
  63. * @param {number|string} [args.tilt_down_max=π/2] - The maximal tilt down angle, in radians (or in degrees if indicated, e.g. `'30deg'`)
  64. * @param {number|string} [args.min_longitude=0] - The minimal longitude to show
  65. * @param {number|string} [args.max_longitude=2π] - The maximal longitude to show
  66. * @param {number} [args.zoom_level=0] - The default zoom level, between 0 and 100
  67. * @param {boolean} [args.smooth_user_moves=true] - If set to `false` user moves have a speed fixed by `long_offset` and `lat_offset`
  68. * @param {number} [args.long_offset=π/360] - The longitude to travel per pixel moved by mouse/touch
  69. * @param {number} [args.lat_offset=π/180] - The latitude to travel per pixel moved by mouse/touch
  70. * @param {number|string} [args.keyboard_long_offset=π/60] - The longitude to travel when the user hits the left/right arrow
  71. * @param {number|string} [args.keyboard_lat_offset=π/120] - The latitude to travel when the user hits the up/down arrow
  72. * @param {integer} [args.time_anim=2000] - Delay before automatically animating the panorama in milliseconds, `false` to not animate
  73. * @param {boolean} [args.reverse_anim=true] - `true` if horizontal animation must be reversed when min/max longitude is reached (only if the whole circle is not described)
  74. * @param {string} [args.anim_speed=2rpm] - Animation speed in radians/degrees/revolutions per second/minute
  75. * @param {string} [args.vertical_anim_speed=2rpm] - Vertical animation speed in radians/degrees/revolutions per second/minute
  76. * @param {number|string} [args.vertical_anim_target=0] - Latitude to target during the autorotate animation, default to the equator
  77. * @param {boolean} [args.navbar=false] - Display the navigation bar if set to `true`
  78. * @param {object} [args.navbar_style] - Style of the navigation bar
  79. * @param {string} [args.navbar_style.backgroundColor=rgba(61, 61, 61, 0.5)] - Navigation bar background color
  80. * @param {string} [args.navbar_style.buttonsColor=rgba(255, 255, 255, 0.7)] - Buttons foreground color
  81. * @param {string} [args.navbar_style.buttonsBackgroundColor=transparent] - Buttons background color
  82. * @param {string} [args.navbar_style.activeButtonsBackgroundColor=rgba(255, 255, 255, 0.1)] - Active buttons background color
  83. * @param {number} [args.navbar_style.buttonsHeight=20] - Buttons height in pixels
  84. * @param {number} [args.navbar_style.autorotateThickness=1] - Autorotate icon thickness in pixels
  85. * @param {number} [args.navbar_style.zoomRangeWidth=50] - Zoom range width in pixels
  86. * @param {number} [args.navbar_style.zoomRangeThickness=1] - Zoom range thickness in pixels
  87. * @param {number} [args.navbar_style.zoomRangeDisk=7] - Zoom range disk diameter in pixels
  88. * @param {number} [args.navbar_style.fullscreenRatio=4/3] - Fullscreen icon ratio (width/height)
  89. * @param {number} [args.navbar_style.fullscreenThickness=2] - Fullscreen icon thickness in pixels
  90. * @param {number} [args.eyes_offset=5] - Eyes offset in VR mode
  91. * @param {string} [args.loading_msg=Loading…] - Loading message
  92. * @param {string} [args.loading_img=null] - Loading image URL or path (absolute or relative)
  93. * @param {HTMLElement|string} [args.loading_html=null] - An HTML loader (element to append to the container or string representing the HTML)
  94. * @param {object} [args.size] - Final size of the panorama container (e.g. {width: 500, height: 300})
  95. * @param {(number|string)} [args.size.width] - Final width in percentage (e.g. `'50%'`) or pixels (e.g. `500` or `'500px'`) ; default to current width
  96. * @param {(number|string)} [args.size.height] - Final height in percentage or pixels ; default to current height
  97. * @param {PhotoSphereViewer~onReady} [args.onready] - Function called once the panorama is ready and the first image is displayed
  98. **/
  99. var PhotoSphereViewer = function(args) {
  100. /**
  101. * Detects whether canvas is supported.
  102. * @private
  103. * @return {boolean} `true` if canvas is supported, `false` otherwise
  104. **/
  105. var isCanvasSupported = function() {
  106. var canvas = document.createElement('canvas');
  107. return !!(canvas.getContext && canvas.getContext('2d'));
  108. };
  109. /**
  110. * Detects whether WebGL is supported.
  111. * @private
  112. * @return {boolean} `true` if WebGL is supported, `false` otherwise
  113. **/
  114. var isWebGLSupported = function() {
  115. var canvas = document.createElement('canvas');
  116. return !!(window.WebGLRenderingContext && canvas.getContext('webgl'));
  117. };
  118. /**
  119. * Attaches an event handler function to an element.
  120. * @private
  121. * @param {HTMLElement} elt - The element
  122. * @param {string} evt - The event name
  123. * @param {function} f - The handler function
  124. * @return {void}
  125. **/
  126. var addEvent = function(elt, evt, f) {
  127. if (!!elt.addEventListener)
  128. elt.addEventListener(evt, f, false);
  129. else
  130. elt.attachEvent('on' + evt, f);
  131. };
  132. /**
  133. * Ensures that a number is in a given interval.
  134. * @private
  135. * @param {number} x - The number to check
  136. * @param {number} min - First endpoint
  137. * @param {number} max - Second endpoint
  138. * @return {number} The checked number
  139. **/
  140. var stayBetween = function(x, min, max) {
  141. return Math.max(min, Math.min(max, x));
  142. };
  143. /**
  144. * Calculates the distance between two points (square of the distance is enough).
  145. * @private
  146. * @param {number} x1 - First point horizontal coordinate
  147. * @param {number} y1 - First point vertical coordinate
  148. * @param {number} x2 - Second point horizontal coordinate
  149. * @param {number} y2 - Second point vertical coordinate
  150. * @return {number} Square of the wanted distance
  151. **/
  152. var dist = function(x1, y1, x2, y2) {
  153. var x = x2 - x1;
  154. var y = y2 - y1;
  155. return x*x + y*y;
  156. };
  157. /**
  158. * Returns the measure of an angle (between 0 and 2π).
  159. * @private
  160. * @param {number} angle - The angle to reduce
  161. * @param {boolean} [is_2pi_allowed=false] - Can the measure be equal to 2π?
  162. * @return {number} The wanted measure
  163. **/
  164. var getAngleMeasure = function(angle, is_2pi_allowed) {
  165. is_2pi_allowed = (is_2pi_allowed !== undefined) ? !!is_2pi_allowed : false;
  166. return (is_2pi_allowed && angle == 2 * Math.PI) ? 2 * Math.PI : angle - Math.floor(angle / (2.0 * Math.PI)) * 2.0 * Math.PI;
  167. };
  168. /**
  169. * Starts to load the panorama.
  170. * @public
  171. * @return {void}
  172. **/
  173. this.load = function() {
  174. container.innerHTML = '';
  175. // Loading HTML: HTMLElement
  176. if (!!loading_html && loading_html.nodeType === 1)
  177. container.appendChild(loading_html);
  178. // Loading HTML: string
  179. else if (!!loading_html && typeof loading_html == 'string')
  180. container.innerHTML = loading_html;
  181. // Loading image
  182. else if (!!loading_img) {
  183. var loading = document.createElement('img');
  184. loading.setAttribute('src', loading_img);
  185. loading.setAttribute('alt', loading_msg);
  186. container.appendChild(loading);
  187. }
  188. // Loading text
  189. else
  190. container.textContent = loading_msg;
  191. // Adds a new container
  192. root = document.createElement('div');
  193. root.style.width = '100%';
  194. root.style.height = '100%';
  195. root.style.position = 'relative';
  196. root.style.overflow = 'hidden';
  197. // Is canvas supported?
  198. if (!isCanvasSupported()) {
  199. container.textContent = 'Canvas is not supported, update your browser!';
  200. return;
  201. }
  202. // Is Three.js loaded?
  203. if (window.THREE === undefined) {
  204. console.log('PhotoSphereViewer: Three.js is not loaded.');
  205. return;
  206. }
  207. // Current viewer size
  208. viewer_size = {
  209. width: 0,
  210. height: 0,
  211. ratio: 0
  212. };
  213. // XMP data?
  214. if (readxmp && !panorama.match(/^data:image\/[a-z]+;base64/))
  215. loadXMP();
  216. else
  217. createBuffer();
  218. };
  219. /**
  220. * Returns Google's XMP data.
  221. * @private
  222. * @param {string} file - Binary file
  223. * @return {string} The data
  224. **/
  225. var getXMPData = function(file) {
  226. var a = 0, b = 0;
  227. var data = '';
  228. while ((a = file.indexOf('<x:xmpmeta', b)) != -1 && (b = file.indexOf('</x:xmpmeta>', a)) != -1) {
  229. data = file.substring(a, b);
  230. if (data.indexOf('GPano:') != -1)
  231. return data;
  232. }
  233. return '';
  234. };
  235. /**
  236. * Returns the value of a given attribute in the panorama metadata.
  237. * @private
  238. * @param {string} data - The panorama metadata
  239. * @param {string} attr - The wanted attribute
  240. * @return {string} The value of the attribute
  241. **/
  242. var getAttribute = function(data, attr) {
  243. var a = data.indexOf('GPano:' + attr) + attr.length + 8, b = data.indexOf('"', a);
  244. if (b == -1) {
  245. // XML-Metadata
  246. a = data.indexOf('GPano:' + attr) + attr.length + 7;
  247. b = data.indexOf('<', a);
  248. }
  249. return data.substring(a, b);
  250. };
  251. /**
  252. * Loads the XMP data with AJAX.
  253. * @private
  254. * @return {void}
  255. **/
  256. var loadXMP = function() {
  257. var xhr = null;
  258. if (window.XMLHttpRequest)
  259. xhr = new XMLHttpRequest();
  260. else if (window.ActiveXObject) {
  261. try {
  262. xhr = new ActiveXObject('Msxml2.XMLHTTP');
  263. }
  264. catch (e) {
  265. xhr = new ActiveXObject('Microsoft.XMLHTTP');
  266. }
  267. }
  268. else {
  269. container.textContent = 'XHR is not supported, update your browser!';
  270. return;
  271. }
  272. xhr.onreadystatechange = function() {
  273. if (xhr.readyState == 4 && xhr.status == 200) {
  274. // Metadata
  275. var data = getXMPData(xhr.responseText);
  276. if (!data.length) {
  277. createBuffer();
  278. return;
  279. }
  280. // Useful values
  281. pano_size = {
  282. full_width: parseInt(getAttribute(data, 'FullPanoWidthPixels')),
  283. full_height: parseInt(getAttribute(data, 'FullPanoHeightPixels')),
  284. cropped_width: parseInt(getAttribute(data, 'CroppedAreaImageWidthPixels')),
  285. cropped_height: parseInt(getAttribute(data, 'CroppedAreaImageHeightPixels')),
  286. cropped_x: parseInt(getAttribute(data, 'CroppedAreaLeftPixels')),
  287. cropped_y: parseInt(getAttribute(data, 'CroppedAreaTopPixels')),
  288. };
  289. recalculate_coords = true;
  290. createBuffer();
  291. }
  292. };
  293. xhr.open('GET', panorama, true);
  294. xhr.send(null);
  295. };
  296. /**
  297. * Creates an image in the right dimensions.
  298. * @private
  299. * @return {void}
  300. **/
  301. var createBuffer = function() {
  302. var img = new Image();
  303. img.onload = function() {
  304. // Must the pano size be changed?
  305. var default_pano_size = {
  306. full_width: img.width,
  307. full_height: img.height,
  308. cropped_width: img.width,
  309. cropped_height: img.height,
  310. cropped_x: null,
  311. cropped_y: null
  312. };
  313. // Captured view?
  314. if (captured_view.horizontal_fov != 360 || captured_view.vertical_fov != 180) {
  315. // The indicated view is the cropped panorama
  316. pano_size.cropped_width = default_pano_size.cropped_width;
  317. pano_size.cropped_height = default_pano_size.cropped_height;
  318. pano_size.full_width = default_pano_size.full_width;
  319. pano_size.full_height = default_pano_size.full_height;
  320. // Horizontal FOV indicated
  321. if (captured_view.horizontal_fov != 360) {
  322. var rh = captured_view.horizontal_fov / 360.0;
  323. pano_size.full_width = pano_size.cropped_width / rh;
  324. }
  325. // Vertical FOV indicated
  326. if (captured_view.vertical_fov != 180) {
  327. var rv = captured_view.vertical_fov / 180.0;
  328. pano_size.full_height = pano_size.cropped_height / rv;
  329. }
  330. }
  331. else {
  332. // Cropped panorama: dimensions defined by the user
  333. for (var attr in pano_size) {
  334. if (pano_size[attr] === null && default_pano_size[attr] !== undefined)
  335. pano_size[attr] = default_pano_size[attr];
  336. }
  337. // Do we have to recalculate the coordinates?
  338. if (recalculate_coords) {
  339. if (pano_size.cropped_width != default_pano_size.cropped_width) {
  340. var rx = default_pano_size.cropped_width / pano_size.cropped_width;
  341. pano_size.cropped_width = default_pano_size.cropped_width;
  342. pano_size.full_width *= rx;
  343. pano_size.cropped_x *= rx;
  344. }
  345. if (pano_size.cropped_height != default_pano_size.cropped_height) {
  346. var ry = default_pano_size.cropped_height / pano_size.cropped_height;
  347. pano_size.cropped_height = default_pano_size.cropped_height;
  348. pano_size.full_height *= ry;
  349. pano_size.cropped_y *= ry;
  350. }
  351. }
  352. }
  353. // Middle if cropped_x/y is null
  354. if (pano_size.cropped_x === null)
  355. pano_size.cropped_x = (pano_size.full_width - pano_size.cropped_width) / 2;
  356. if (pano_size.cropped_y === null)
  357. pano_size.cropped_y = (pano_size.full_height - pano_size.cropped_height) / 2;
  358. // Size limit for mobile compatibility
  359. var max_width = 2048;
  360. if (isWebGLSupported()) {
  361. var canvas_tmp = document.createElement('canvas');
  362. var ctx_tmp = canvas_tmp.getContext('webgl');
  363. max_width = ctx_tmp.getParameter(ctx_tmp.MAX_TEXTURE_SIZE);
  364. }
  365. // Buffer width (not too big)
  366. var new_width = Math.min(pano_size.full_width, max_width);
  367. var r = new_width / pano_size.full_width;
  368. pano_size.full_width = new_width;
  369. pano_size.cropped_width *= r;
  370. pano_size.cropped_x *= r;
  371. img.width = pano_size.cropped_width;
  372. // Buffer height (proportional to the width)
  373. pano_size.full_height *= r;
  374. pano_size.cropped_height *= r;
  375. pano_size.cropped_y *= r;
  376. img.height = pano_size.cropped_height;
  377. // Buffer creation
  378. var buffer = document.createElement('canvas');
  379. buffer.width = pano_size.full_width;
  380. buffer.height = pano_size.full_height;
  381. var ctx = buffer.getContext('2d');
  382. ctx.drawImage(img, pano_size.cropped_x, pano_size.cropped_y, pano_size.cropped_width, pano_size.cropped_height);
  383. loadTexture(buffer.toDataURL('image/jpeg'));
  384. };
  385. // CORS when the panorama is not given as a base64 string
  386. if (cors_anonymous && !panorama.match(/^data:image\/[a-z]+;base64/))
  387. img.setAttribute('crossOrigin', 'anonymous');
  388. img.src = panorama;
  389. };
  390. /**
  391. * Loads the sphere texture.
  392. * @private
  393. * @param {string} path - Path to the panorama
  394. * @return {void}
  395. **/
  396. var loadTexture = function(path) {
  397. var texture = new THREE.Texture();
  398. var loader = new THREE.ImageLoader();
  399. var onLoad = function(img) {
  400. texture.needsUpdate = true;
  401. texture.image = img;
  402. createScene(texture);
  403. };
  404. loader.load(path, onLoad);
  405. };
  406. /**
  407. * Creates the 3D scene.
  408. * @private
  409. * @param {THREE.Texture} texture - The sphere texture
  410. * @return {void}
  411. **/
  412. var createScene = function(texture) {
  413. // New size?
  414. if (new_viewer_size.width !== undefined)
  415. container.style.width = new_viewer_size.width.css;
  416. if (new_viewer_size.height !== undefined)
  417. container.style.height = new_viewer_size.height.css;
  418. fitToContainer();
  419. // The chosen renderer depends on whether WebGL is supported or not
  420. renderer = (isWebGLSupported()) ? new THREE.WebGLRenderer() : new THREE.CanvasRenderer();
  421. renderer.setSize(viewer_size.width, viewer_size.height);
  422. scene = new THREE.Scene();
  423. camera = new THREE.PerspectiveCamera(PSV_FOV_MAX, viewer_size.ratio, 1, 300);
  424. camera.position.set(0, 0, 0);
  425. scene.add(camera);
  426. // Sphere
  427. var geometry = new THREE.SphereGeometry(200, rings, segments);
  428. var material = new THREE.MeshBasicMaterial({map: texture, overdraw: true});
  429. var mesh = new THREE.Mesh(geometry, material);
  430. mesh.scale.x = -1;
  431. scene.add(mesh);
  432. // Canvas container
  433. canvas_container = document.createElement('div');
  434. canvas_container.style.position = 'absolute';
  435. canvas_container.style.zIndex = 0;
  436. root.appendChild(canvas_container);
  437. // Navigation bar?
  438. if (display_navbar) {
  439. navbar.setStyle(navbar_style);
  440. navbar.create();
  441. root.appendChild(navbar.getBar());
  442. }
  443. // Overlay?
  444. if (overlay !== null) {
  445. // Add the image
  446. var overlay_img = document.createElement('img');
  447. overlay_img.onload = function() {
  448. overlay_img.style.display = 'block';
  449. // Image position
  450. overlay_img.style.position = 'absolute';
  451. overlay_img.style[overlay.position.x] = '5px';
  452. overlay_img.style[overlay.position.y] = '5px';
  453. if (overlay.position.y == 'bottom' && display_navbar)
  454. overlay_img.style.bottom = (navbar.getBar().offsetHeight + 5) + 'px';
  455. // Should we resize the image?
  456. if (overlay.size !== undefined) {
  457. overlay_img.style.width = overlay.size.width;
  458. overlay_img.style.height = overlay.size.height;
  459. }
  460. root.appendChild(overlay_img);
  461. };
  462. overlay_img.src = overlay.image;
  463. }
  464. // Adding events
  465. addEvent(window, 'resize', fitToContainer);
  466. if (user_interactions_allowed) {
  467. addEvent(canvas_container, 'mousedown', onMouseDown);
  468. addEvent(document, 'mousemove', onMouseMove);
  469. addEvent(canvas_container, 'mousemove', showNavbar);
  470. addEvent(document, 'mouseup', onMouseUp);
  471. addEvent(canvas_container, 'touchstart', onTouchStart);
  472. addEvent(document, 'touchend', onMouseUp);
  473. addEvent(document, 'touchmove', onTouchMove);
  474. if (scroll_to_zoom) {
  475. addEvent(canvas_container, 'mousewheel', onMouseWheel);
  476. addEvent(canvas_container, 'DOMMouseScroll', onMouseWheel);
  477. }
  478. self.addAction('fullscreen-mode', toggleArrowKeys);
  479. }
  480. addEvent(document, 'fullscreenchange', fullscreenToggled);
  481. addEvent(document, 'mozfullscreenchange', fullscreenToggled);
  482. addEvent(document, 'webkitfullscreenchange', fullscreenToggled);
  483. addEvent(document, 'MSFullscreenChange', fullscreenToggled);
  484. sphoords.addListener(onDeviceOrientation);
  485. // First render
  486. container.innerHTML = '';
  487. container.appendChild(root);
  488. var canvas = renderer.domElement;
  489. canvas.style.display = 'block';
  490. canvas_container.appendChild(canvas);
  491. render();
  492. // Zoom?
  493. if (zoom_lvl > 0)
  494. zoom(zoom_lvl);
  495. // Animation?
  496. anim();
  497. /**
  498. * Indicates that the loading is finished: the first image is rendered
  499. * @callback PhotoSphereViewer~onReady
  500. **/
  501. triggerAction('ready');
  502. };
  503. /**
  504. * Renders an image.
  505. * @private
  506. * @return {void}
  507. **/
  508. var render = function() {
  509. var point = new THREE.Vector3();
  510. point.setX(Math.cos(lat) * Math.sin(long));
  511. point.setY(Math.sin(lat));
  512. point.setZ(Math.cos(lat) * Math.cos(long));
  513. camera.lookAt(point);
  514. // Stereo?
  515. if (stereo_effect !== null)
  516. stereo_effect.render(scene, camera);
  517. else
  518. renderer.render(scene, camera);
  519. };
  520. /**
  521. * Starts the stereo effect.
  522. * @private
  523. * @return {void}
  524. **/
  525. var startStereo = function() {
  526. stereo_effect = new THREE.StereoEffect(renderer);
  527. stereo_effect.eyeSeparation = eyes_offset;
  528. stereo_effect.setSize(viewer_size.width, viewer_size.height);
  529. startDeviceOrientation();
  530. enableFullscreen();
  531. navbar.mustBeHidden();
  532. render();
  533. /**
  534. * Indicates that the stereo effect has been toggled.
  535. * @callback PhotoSphereViewer~onStereoEffectToggled
  536. * @param {boolean} enabled - `true` if stereo effect is enabled, `false` otherwise
  537. **/
  538. triggerAction('stereo-effect', true);
  539. };
  540. /**
  541. * Stops the stereo effect.
  542. * @private
  543. * @return {void}
  544. **/
  545. var stopStereo = function() {
  546. stereo_effect = null;
  547. renderer.setSize(viewer_size.width, viewer_size.height);
  548. navbar.mustBeHidden(false);
  549. render();
  550. triggerAction('stereo-effect', false);
  551. };
  552. /**
  553. * Toggles the stereo effect (virtual reality).
  554. * @public
  555. * @return {void}
  556. **/
  557. this.toggleStereo = function() {
  558. if (stereo_effect !== null)
  559. stopStereo();
  560. else
  561. startStereo();
  562. };
  563. /**
  564. * Automatically animates the panorama.
  565. * @private
  566. * @return {void}
  567. **/
  568. var anim = function() {
  569. if (anim_delay !== false)
  570. anim_timeout = setTimeout(startAutorotate, anim_delay);
  571. };
  572. /**
  573. * Automatically rotates the panorama.
  574. * @private
  575. * @return {void}
  576. **/
  577. var autorotate = function() {
  578. lat -= (lat - anim_lat_target) * anim_lat_offset;
  579. long += anim_long_offset;
  580. var again = true;
  581. if (!whole_circle) {
  582. long = stayBetween(long, PSV_MIN_LONGITUDE, PSV_MAX_LONGITUDE);
  583. if (long == PSV_MIN_LONGITUDE || long == PSV_MAX_LONGITUDE) {
  584. // Must we reverse the animation or simply stop it?
  585. if (reverse_anim)
  586. anim_long_offset *= -1;
  587. else {
  588. stopAutorotate();
  589. again = false;
  590. }
  591. }
  592. }
  593. long = getAngleMeasure(long, true);
  594. triggerAction('position-updated', {
  595. longitude: long,
  596. latitude: lat
  597. });
  598. render();
  599. if (again)
  600. autorotate_timeout = setTimeout(autorotate, PSV_ANIM_TIMEOUT);
  601. };
  602. /**
  603. * Starts the autorotate animation.
  604. * @private
  605. * @return {void}
  606. **/
  607. var startAutorotate = function() {
  608. autorotate();
  609. /**
  610. * Indicates that the autorotate animation state has changed.
  611. * @callback PhotoSphereViewer~onAutorotateChanged
  612. * @param {boolean} enabled - `true` if animation is enabled, `false` otherwise
  613. **/
  614. triggerAction('autorotate', true);
  615. };
  616. /**
  617. * Stops the autorotate animation.
  618. * @private
  619. * @return {void}
  620. **/
  621. var stopAutorotate = function() {
  622. clearTimeout(anim_timeout);
  623. anim_timeout = null;
  624. clearTimeout(autorotate_timeout);
  625. autorotate_timeout = null;
  626. triggerAction('autorotate', false);
  627. };
  628. /**
  629. * Launches/stops the autorotate animation.
  630. * @public
  631. * @return {void}
  632. **/
  633. this.toggleAutorotate = function() {
  634. clearTimeout(anim_timeout);
  635. if (!!autorotate_timeout)
  636. stopAutorotate();
  637. else
  638. startAutorotate();
  639. };
  640. /**
  641. * Resizes the canvas to make it fit the container.
  642. * @private
  643. * @return {void}
  644. **/
  645. var fitToContainer = function() {
  646. if (container.clientWidth != viewer_size.width || container.clientHeight != viewer_size.height) {
  647. resize({
  648. width: container.clientWidth,
  649. height: container.clientHeight
  650. });
  651. }
  652. };
  653. /**
  654. * Resizes the canvas to make it fit the container.
  655. * @public
  656. * @return {void}
  657. **/
  658. this.fitToContainer = function() {
  659. fitToContainer();
  660. };
  661. /**
  662. * Resizes the canvas.
  663. * @private
  664. * @param {object} size - New dimensions
  665. * @param {number} [size.width] - The new canvas width (default to previous width)
  666. * @param {number} [size.height] - The new canvas height (default to previous height)
  667. * @return {void}
  668. **/
  669. var resize = function(size) {
  670. viewer_size.width = (size.width !== undefined) ? parseInt(size.width) : viewer_size.width;
  671. viewer_size.height = (size.height !== undefined) ? parseInt(size.height) : viewer_size.height;
  672. viewer_size.ratio = viewer_size.width / viewer_size.height;
  673. if (!!camera) {
  674. camera.aspect = viewer_size.ratio;
  675. camera.updateProjectionMatrix();
  676. }
  677. if (!!renderer) {
  678. renderer.setSize(viewer_size.width, viewer_size.height);
  679. render();
  680. }
  681. if (!!stereo_effect) {
  682. stereo_effect.setSize(viewer_size.width, viewer_size.height);
  683. render();
  684. }
  685. };
  686. /**
  687. * Returns the current position in radians
  688. * @return {object} A longitude/latitude couple
  689. **/
  690. this.getPosition = function() {
  691. return {
  692. longitude: long,
  693. latitude: lat
  694. };
  695. };
  696. /**
  697. * Returns the current position in degrees
  698. * @return {object} A longitude/latitude couple
  699. **/
  700. this.getPositionInDegrees = function() {
  701. return {
  702. longitude: long * 180.0 / Math.PI,
  703. latitude: lat * 180.0 / Math.PI
  704. };
  705. };
  706. /**
  707. * Moves to a specific position
  708. * @private
  709. * @param {number|string} longitude - The longitude of the targeted point
  710. * @param {number|string} latitude - The latitude of the targeted point
  711. * @return {void}
  712. **/
  713. var moveTo = function(longitude, latitude) {
  714. var long_tmp = parseAngle(longitude);
  715. if (!whole_circle)
  716. long_tmp = stayBetween(long_tmp, PSV_MIN_LONGITUDE, PSV_MAX_LONGITUDE);
  717. var lat_tmp = parseAngle(latitude);
  718. if (lat_tmp > Math.PI)
  719. lat_tmp -= 2 * Math.PI;
  720. lat_tmp = stayBetween(lat_tmp, PSV_TILT_DOWN_MAX, PSV_TILT_UP_MAX);
  721. long = long_tmp;
  722. lat = lat_tmp;
  723. /**
  724. * Indicates that the position has been modified.
  725. * @callback PhotoSphereViewer~onPositionUpdateed
  726. * @param {object} position - The new position
  727. * @param {number} position.longitude - The longitude in radians
  728. * @param {number} position.latitude - The latitude in radians
  729. **/
  730. triggerAction('position-updated', {
  731. longitude: long,
  732. latitude: lat
  733. });
  734. render();
  735. };
  736. /**
  737. * Moves to a specific position
  738. * @public
  739. * @param {number|string} longitude - The longitude of the targeted point
  740. * @param {number|string} latitude - The latitude of the targeted point
  741. * @return {void}
  742. **/
  743. this.moveTo = function(longitude, latitude) {
  744. moveTo(longitude, latitude);
  745. };
  746. /**
  747. * Rotates the view
  748. * @private
  749. * @param {number|string} dlong - The rotation to apply horizontally
  750. * @param {number|string} dlat - The rotation to apply vertically
  751. * @return {void}
  752. **/
  753. var rotate = function(dlong, dlat) {
  754. dlong = parseAngle(dlong);
  755. dlat = parseAngle(dlat);
  756. moveTo(long + dlong, lat + dlat);
  757. };
  758. /**
  759. * Rotates the view
  760. * @public
  761. * @param {number|string} dlong - The rotation to apply horizontally
  762. * @param {number|string} dlat - The rotation to apply vertically
  763. * @return {void}
  764. **/
  765. this.rotate = function(dlong, dlat) {
  766. rotate(dlong, dlat);
  767. };
  768. /**
  769. * Attaches or detaches the keyboard events
  770. * @private
  771. * @param {boolean} attach - `true` to attach the event, `false` to detach it
  772. * @return {void}
  773. **/
  774. var toggleArrowKeys = function(attach) {
  775. var action = (attach) ? window.addEventListener : window.removeEventListener;
  776. action('keydown', keyDown);
  777. };
  778. /**
  779. * Tries to standardize the code sent by a keyboard event
  780. * @private
  781. * @param {KeyboardEvent} evt - The event
  782. * @return {string} The code
  783. **/
  784. var retrieveKey = function(evt) {
  785. // The Holy Grail
  786. if (evt.key) {
  787. var key = (/^Arrow/.test(evt.key)) ? evt.key : 'Arrow' + evt.key;
  788. return key;
  789. }
  790. // Deprecated but still used
  791. if (evt.keyCode || evt.which) {
  792. var key_code = (evt.keyCode) ? evt.keyCode : evt.which;
  793. var keycodes_map = {
  794. 38: 'ArrowUp',
  795. 39: 'ArrowRight',
  796. 40: 'ArrowDown',
  797. 37: 'ArrowLeft'
  798. };
  799. if (keycodes_map[key_code] !== undefined)
  800. return keycodes_map[key_code];
  801. }
  802. // :/
  803. return '';
  804. };
  805. /**
  806. * Rotates the view through keyboard arrows
  807. * @private
  808. * @param {KeyboardEvent} evt - The event
  809. * @return {void}
  810. **/
  811. var keyDown = function(evt) {
  812. var dlong = 0, dlat = 0;
  813. switch (retrieveKey(evt)) {
  814. case 'ArrowUp':
  815. dlat = PSV_KEYBOARD_LAT_OFFSET;
  816. break;
  817. case 'ArrowRight':
  818. dlong = -PSV_KEYBOARD_LONG_OFFSET;
  819. break;
  820. case 'ArrowDown':
  821. dlat = -PSV_KEYBOARD_LAT_OFFSET;
  822. break;
  823. case 'ArrowLeft':
  824. dlong = PSV_KEYBOARD_LONG_OFFSET;
  825. break;
  826. }
  827. rotate(dlong, dlat);
  828. };
  829. /**
  830. * The user wants to move.
  831. * @private
  832. * @param {Event} evt - The event
  833. * @return {void}
  834. **/
  835. var onMouseDown = function(evt) {
  836. startMove(parseInt(evt.clientX), parseInt(evt.clientY));
  837. };
  838. /**
  839. * The user wants to move or to zoom (mobile version).
  840. * @private
  841. * @param {Event} evt - The event
  842. * @return {void}
  843. **/
  844. var onTouchStart = function(evt) {
  845. // Move
  846. if (evt.touches.length == 1) {
  847. var touch = evt.touches[0];
  848. if (touch.target.parentNode == canvas_container)
  849. startMove(parseInt(touch.clientX), parseInt(touch.clientY));
  850. }
  851. // Zoom
  852. else if (evt.touches.length == 2) {
  853. onMouseUp();
  854. if (evt.touches[0].target.parentNode == canvas_container && evt.touches[1].target.parentNode == canvas_container)
  855. startTouchZoom(dist(evt.touches[0].clientX, evt.touches[0].clientY, evt.touches[1].clientX, evt.touches[1].clientY));
  856. }
  857. // Show navigation bar if hidden
  858. showNavbar();
  859. };
  860. /**
  861. * Initializes the movement.
  862. * @private
  863. * @param {integer} x - Horizontal coordinate
  864. * @param {integer} y - Vertical coordinate
  865. * @return {void}
  866. **/
  867. var startMove = function(x, y) {
  868. // Store the current position of the mouse
  869. mouse_x = x;
  870. mouse_y = y;
  871. // Stop the animation
  872. stopAutorotate();
  873. // Start the movement
  874. mousedown = true;
  875. };
  876. /**
  877. * Initializes the "pinch to zoom" action.
  878. * @private
  879. * @param {number} d - Square of the distance between the two fingers
  880. * @return {void}
  881. **/
  882. var startTouchZoom = function(d) {
  883. touchzoom_dist = d;
  884. touchzoom = true;
  885. };
  886. /**
  887. * The user wants to stop moving (or stop zooming with their finger).
  888. * @private
  889. * @param {Event} evt - The event
  890. * @return {void}
  891. **/
  892. var onMouseUp = function(evt) {
  893. mousedown = false;
  894. touchzoom = false;
  895. };
  896. /**
  897. * The user moves the image.
  898. * @private
  899. * @param {Event} evt - The event
  900. * @return {void}
  901. **/
  902. var onMouseMove = function(evt) {
  903. // evt.preventDefault();
  904. move(parseInt(evt.clientX), parseInt(evt.clientY));
  905. };
  906. /**
  907. * The user moves the image (mobile version).
  908. * @private
  909. * @param {Event} evt - The event
  910. * @return {void}
  911. **/
  912. var onTouchMove = function(evt) {
  913. // Move
  914. if (evt.touches.length == 1 && mousedown) {
  915. var touch = evt.touches[0];
  916. if (touch.target.parentNode == canvas_container) {
  917. evt.preventDefault();
  918. move(parseInt(touch.clientX), parseInt(touch.clientY));
  919. }
  920. }
  921. // Zoom
  922. else if (evt.touches.length == 2) {
  923. if (evt.touches[0].target.parentNode == canvas_container && evt.touches[1].target.parentNode == canvas_container && touchzoom) {
  924. evt.preventDefault();
  925. // Calculate the new level of zoom
  926. var d = dist(evt.touches[0].clientX, evt.touches[0].clientY, evt.touches[1].clientX, evt.touches[1].clientY);
  927. var diff = d - touchzoom_dist;
  928. if (diff !== 0) {
  929. var direction = diff / Math.abs(diff);
  930. zoom(zoom_lvl + direction * zoom_speed);
  931. touchzoom_dist = d;
  932. }
  933. }
  934. }
  935. };
  936. /**
  937. * Movement.
  938. * @private
  939. * @param {integer} x - Horizontal coordinate
  940. * @param {integer} y - Vertical coordinate
  941. * @return {void}
  942. **/
  943. var move = function(x, y) {
  944. if (mousedown) {
  945. // Smooth movement
  946. if (smooth_user_moves) {
  947. long += (x - mouse_x) / viewer_size.height * fov * Math.PI / 180;
  948. lat += (y - mouse_y) / viewer_size.height * fov * Math.PI / 180;
  949. }
  950. // No smooth movement
  951. else {
  952. long += (x - mouse_x) * PSV_LONG_OFFSET;
  953. lat += (y - mouse_y) * PSV_LAT_OFFSET;
  954. }
  955. // Save the current coordinates for the next movement
  956. mouse_x = x;
  957. mouse_y = y;
  958. // Coordinates treatments
  959. if (!whole_circle)
  960. long = stayBetween(long, PSV_MIN_LONGITUDE, PSV_MAX_LONGITUDE);
  961. long = getAngleMeasure(long, true);
  962. lat = stayBetween(lat, PSV_TILT_DOWN_MAX, PSV_TILT_UP_MAX);
  963. triggerAction('position-updated', {
  964. longitude: long,
  965. latitude: lat
  966. });
  967. render();
  968. }
  969. };
  970. /**
  971. * Starts following the device orientation.
  972. * @private
  973. * @return {void}
  974. **/
  975. var startDeviceOrientation = function() {
  976. sphoords.start();
  977. stopAutorotate();
  978. /**
  979. * Indicates that we starts/stops following the device orientation.
  980. * @callback PhotoSphereViewer~onDeviceOrientationStateChanged
  981. * @param {boolean} state - `true` if device orientation is followed, `false` otherwise
  982. **/
  983. triggerAction('device-orientation', true);
  984. };
  985. /**
  986. * Stops following the device orientation.
  987. * @private
  988. * @return {void}
  989. **/
  990. var stopDeviceOrientation = function() {
  991. sphoords.stop();
  992. triggerAction('device-orientation', false);
  993. };
  994. /**
  995. * Starts/stops following the device orientation.
  996. * @public
  997. * @return {void}
  998. **/
  999. this.toggleDeviceOrientation = function() {
  1000. if (sphoords.isEventAttached())
  1001. stopDeviceOrientation();
  1002. else
  1003. startDeviceOrientation();
  1004. };
  1005. /**
  1006. * The user moved their device.
  1007. * @private
  1008. * @param {object} coords - The spherical coordinates to look at
  1009. * @param {number} coords.longitude - The longitude
  1010. * @param {number} coords.latitude - The latitude
  1011. * @return {void}
  1012. **/
  1013. var onDeviceOrientation = function(coords) {
  1014. long = stayBetween(coords.longitude, PSV_MIN_LONGITUDE, PSV_MAX_LONGITUDE);
  1015. lat = stayBetween(coords.latitude, PSV_TILT_DOWN_MAX, PSV_TILT_UP_MAX);
  1016. triggerAction('position-updated', {
  1017. longitude: long,
  1018. latitude: lat
  1019. });
  1020. render();
  1021. };
  1022. /**
  1023. * The user wants to zoom.
  1024. * @private
  1025. * @param {Event} evt - The event
  1026. * @return {void}
  1027. **/
  1028. var onMouseWheel = function(evt) {
  1029. evt.preventDefault();
  1030. evt.stopPropagation();
  1031. var delta = (evt.detail) ? -evt.detail : evt.wheelDelta;
  1032. if (delta !== 0) {
  1033. var direction = parseInt(delta / Math.abs(delta));
  1034. zoom(zoom_lvl + direction * zoom_speed);
  1035. }
  1036. };
  1037. /**
  1038. * Use a mousewheel event.
  1039. * @public
  1040. * @param {Event} evt - The event
  1041. * @return {void}
  1042. **/
  1043. this.mouseWheel = function(evt) {
  1044. onMouseWheel(evt);
  1045. };
  1046. /**
  1047. * Sets the new zoom level.
  1048. * @private
  1049. * @param {integer} level - New zoom level
  1050. * @return {void}
  1051. **/
  1052. var zoom = function(level) {
  1053. zoom_lvl = stayBetween(level, 0, 100);
  1054. fov = PSV_FOV_MAX + (zoom_lvl / 100) * (PSV_FOV_MIN - PSV_FOV_MAX);
  1055. camera.fov = fov;
  1056. camera.updateProjectionMatrix();
  1057. render();
  1058. /**
  1059. * Indicates that the zoom level has changed.
  1060. * @callback PhotoSphereViewer~onZoomUpdated
  1061. * @param {number} zoom_level - The new zoom level
  1062. **/
  1063. triggerAction('zoom-updated', zoom_lvl);
  1064. };
  1065. /**
  1066. * Returns the current zoom level.
  1067. * @public
  1068. * @return {integer} The current zoom level (between 0 and 100)
  1069. **/
  1070. this.getZoomLevel = function() {
  1071. return zoom_lvl;
  1072. };
  1073. /**
  1074. * Sets the new zoom level.
  1075. * @public
  1076. * @param {integer} level - New zoom level
  1077. * @return {void}
  1078. **/
  1079. this.zoom = function(level) {
  1080. zoom(level);
  1081. };
  1082. /**
  1083. * Zoom in.
  1084. * @public
  1085. * @return {void}
  1086. **/
  1087. this.zoomIn = function() {
  1088. if (zoom_lvl < 100)
  1089. zoom(zoom_lvl + zoom_speed);
  1090. };
  1091. /**
  1092. * Zoom out.
  1093. * @public
  1094. * @return {void}
  1095. **/
  1096. this.zoomOut = function() {
  1097. if (zoom_lvl > 0)
  1098. zoom(zoom_lvl - zoom_speed);
  1099. };
  1100. /**
  1101. * Detects whether fullscreen is enabled or not.
  1102. * @private
  1103. * @return {boolean} `true` if fullscreen is enabled, `false` otherwise
  1104. **/
  1105. var isFullscreenEnabled = function() {
  1106. return (!!document.fullscreenElement || !!document.mozFullScreenElement || !!document.webkitFullscreenElement || !!document.msFullscreenElement);
  1107. };
  1108. /**
  1109. * Fullscreen state has changed.
  1110. * @private
  1111. * @return {void}
  1112. **/
  1113. var fullscreenToggled = function() {
  1114. // Fix the (weird and ugly) Chrome and IE behaviors
  1115. if (!!document.webkitFullscreenElement || !!document.msFullscreenElement) {
  1116. real_viewer_size.width = container.style.width;
  1117. real_viewer_size.height = container.style.height;
  1118. container.style.width = '100%';
  1119. container.style.height = '100%';
  1120. fitToContainer();
  1121. }
  1122. else if (!!container.webkitRequestFullscreen || !!container.msRequestFullscreen) {
  1123. container.style.width = real_viewer_size.width;
  1124. container.style.height = real_viewer_size.height;
  1125. fitToContainer();
  1126. }
  1127. /**
  1128. * Indicates that the fullscreen mode has been toggled.
  1129. * @callback PhotoSphereViewer~onFullscreenToggled
  1130. * @param {boolean} enabled - `true` if fullscreen is enabled, `false` otherwise
  1131. **/
  1132. triggerAction('fullscreen-mode', isFullscreenEnabled());
  1133. };
  1134. /**
  1135. * Enables fullscreen.
  1136. * @private
  1137. * @return {void}
  1138. **/
  1139. var enableFullscreen = function() {
  1140. if (!!container.requestFullscreen)
  1141. container.requestFullscreen();
  1142. else if (!!container.mozRequestFullScreen)
  1143. container.mozRequestFullScreen();
  1144. else if (!!container.webkitRequestFullscreen)
  1145. container.webkitRequestFullscreen();
  1146. else if (!!container.msRequestFullscreen)
  1147. container.msRequestFullscreen();
  1148. };
  1149. /**
  1150. * Disables fullscreen.
  1151. * @private
  1152. * @return {void}
  1153. **/
  1154. var disableFullscreen = function() {
  1155. if (!!document.exitFullscreen)
  1156. document.exitFullscreen();
  1157. else if (!!document.mozCancelFullScreen)
  1158. document.mozCancelFullScreen();
  1159. else if (!!document.webkitExitFullscreen)
  1160. document.webkitExitFullscreen();
  1161. else if (!!document.msExitFullscreen)
  1162. document.msExitFullscreen();
  1163. };
  1164. /**
  1165. * Enables/disables fullscreen.
  1166. * @public
  1167. * @return {void}
  1168. **/
  1169. this.toggleFullscreen = function() {
  1170. // Switches to fullscreen mode
  1171. if (!isFullscreenEnabled())
  1172. enableFullscreen();
  1173. // Switches to windowed mode
  1174. else
  1175. disableFullscreen();
  1176. };
  1177. /**
  1178. * Shows the navigation bar.
  1179. * @private
  1180. * @return {void}
  1181. **/
  1182. var showNavbar = function() {
  1183. if (display_navbar)
  1184. navbar.show();
  1185. };
  1186. /**
  1187. * Parses an animation speed.
  1188. * @private
  1189. * @param {string} speed - The speed, in radians/degrees/revolutions per second/minute
  1190. * @return {number} The speed in radians
  1191. **/
  1192. var parseAnimationSpeed = function(speed) {
  1193. speed = speed.toString().trim();
  1194. // Speed extraction
  1195. var speed_value = parseFloat(speed.replace(/^(-?[0-9]+(?:\.[0-9]*)?).*$/, '$1'));
  1196. var speed_unit = speed.replace(/^-?[0-9]+(?:\.[0-9]*)?(.*)$/, '$1').trim();
  1197. // "per minute" -> "per second"
  1198. if (speed_unit.match(/(pm|per minute)$/))
  1199. speed_value /= 60;
  1200. var rad_per_second = 0;
  1201. // Which unit?
  1202. switch (speed_unit) {
  1203. // Revolutions per minute / second
  1204. case 'rpm':
  1205. case 'rev per minute':
  1206. case 'revolutions per minute':
  1207. case 'rps':
  1208. case 'rev per second':
  1209. case 'revolutions per second':
  1210. // speed * 2pi
  1211. rad_per_second = speed_value * 2 * Math.PI;
  1212. break;
  1213. // Degrees per minute / second
  1214. case 'dpm':
  1215. case 'deg per minute':
  1216. case 'degrees per minute':
  1217. case 'dps':
  1218. case 'deg per second':
  1219. case 'degrees per second':
  1220. // Degrees to radians (rad = deg * pi / 180)
  1221. rad_per_second = speed_value * Math.PI / 180;
  1222. break;
  1223. // Radians per minute / second
  1224. case 'rad per minute':
  1225. case 'radians per minute':
  1226. case 'rad per second':
  1227. case 'radians per second':
  1228. rad_per_second = speed_value;
  1229. break;
  1230. // Unknown unit
  1231. default:
  1232. m_anim = false;
  1233. }
  1234. // Longitude offset
  1235. return rad_per_second * PSV_ANIM_TIMEOUT / 1000;
  1236. };
  1237. /**
  1238. * Parses an angle given in radians or degrees.
  1239. * @private
  1240. * @param {number|string} angle - Angle in radians (number) or in degrees (string)
  1241. * @return {number} The angle in radians
  1242. **/
  1243. var parseAngle = function(angle) {
  1244. angle = angle.toString().trim();
  1245. // Angle extraction
  1246. var angle_value = parseFloat(angle.replace(/^(-?[0-9]+(?:\.[0-9]*)?).*$/, '$1'));
  1247. var angle_unit = angle.replace(/^-?[0-9]+(?:\.[0-9]*)?(.*)$/, '$1').trim();
  1248. // Degrees
  1249. if (angle_unit == 'deg')
  1250. angle_value *= Math.PI / 180;
  1251. // Radians by default, we don't have anyting to do
  1252. return getAngleMeasure(angle_value);
  1253. };
  1254. /**
  1255. * Sets the viewer size.
  1256. * @private
  1257. * @param {object} size - An object containing the wanted width and height
  1258. * @return {void}
  1259. **/
  1260. var setNewViewerSize = function(size) {
  1261. // Checks all the values
  1262. for (var dim in size) {
  1263. // Only width and height matter
  1264. if (dim == 'width' || dim == 'height') {
  1265. // Size extraction
  1266. var size_str = size[dim].toString().trim();
  1267. var size_value = parseFloat(size_str.replace(/^([0-9]+(?:\.[0-9]*)?).*$/, '$1'));
  1268. var size_unit = size_str.replace(/^[0-9]+(?:\.[0-9]*)?(.*)$/, '$1').trim();
  1269. // Only percentages and pixels are allowed
  1270. if (size_unit != '%')
  1271. size_unit = 'px';
  1272. // We're good
  1273. new_viewer_size[dim] = {
  1274. css: size_value + size_unit,
  1275. unit: size_unit
  1276. };
  1277. }
  1278. }
  1279. };
  1280. /**
  1281. * Adds a function to execute when a given action occurs.
  1282. * @public
  1283. * @param {string} name - The action name
  1284. * @param {function} f - The handler function
  1285. * @return {void}
  1286. **/
  1287. this.addAction = function(name, f) {
  1288. // New action?
  1289. if (!(name in actions))
  1290. actions[name] = [];
  1291. actions[name].push(f);
  1292. };
  1293. /**
  1294. * Triggers an action.
  1295. * @private
  1296. * @param {string} name - Action name
  1297. * @param {*} arg - An argument to send to the handler functions
  1298. * @return {void}
  1299. **/
  1300. var triggerAction = function(name, arg) {
  1301. // Does the action have any function?
  1302. if ((name in actions) && !!actions[name].length) {
  1303. for (var i = 0, l = actions[name].length; i < l; ++i) {
  1304. if (arg !== undefined)
  1305. actions[name][i](arg);
  1306. else
  1307. actions[name][i]();
  1308. }
  1309. }
  1310. };
  1311. // Required parameters
  1312. if (args === undefined || args.panorama === undefined || args.container === undefined) {
  1313. console.log('PhotoSphereViewer: no value given for panorama or container');
  1314. return;
  1315. }
  1316. // Should the movement be smooth?
  1317. var smooth_user_moves = (args.smooth_user_moves !== undefined) ? !!args.smooth_user_moves : true;
  1318. // Movement speed
  1319. var PSV_LONG_OFFSET = (args.long_offset !== undefined) ? parseAngle(args.long_offset) : Math.PI / 360.0;
  1320. var PSV_LAT_OFFSET = (args.lat_offset !== undefined) ? parseAngle(args.lat_offset) : Math.PI / 180.0;
  1321. var PSV_KEYBOARD_LONG_OFFSET = (args.keyboard_long_offset !== undefined) ? parseAngle(args.keyboard_long_offset) : Math.PI / 60.0;
  1322. var PSV_KEYBOARD_LAT_OFFSET = (args.keyboard_lat_offset !== undefined) ? parseAngle(args.keyboard_lat_offset) : Math.PI / 120.0;
  1323. // Minimum and maximum fields of view in degrees
  1324. var PSV_FOV_MIN = (args.min_fov !== undefined) ? stayBetween(parseFloat(args.min_fov), 1, 179) : 30;
  1325. var PSV_FOV_MAX = (args.max_fov !== undefined) ? stayBetween(parseFloat(args.max_fov), 1, 179) : 90;
  1326. // Minimum tilt up / down angles
  1327. var PSV_TILT_UP_MAX = (args.tilt_up_max !== undefined) ? stayBetween(parseAngle(args.tilt_up_max), 0, Math.PI / 2.0) : Math.PI / 2.0;
  1328. var PSV_TILT_DOWN_MAX = (args.tilt_down_max !== undefined) ? -stayBetween(parseAngle(args.tilt_down_max), 0, Math.PI / 2.0) : -Math.PI / 2.0;
  1329. // Minimum and maximum visible longitudes
  1330. var min_long = (args.min_longitude !== undefined) ? parseAngle(args.min_longitude) : 0;
  1331. var max_long = (args.max_longitude !== undefined) ? parseAngle(args.max_longitude) : 0;
  1332. var whole_circle = (min_long == max_long);
  1333. if (whole_circle) {
  1334. min_long = 0;
  1335. max_long = 2 * Math.PI;
  1336. }
  1337. else if (max_long === 0)
  1338. max_long = 2 * Math.PI;
  1339. var PSV_MIN_LONGITUDE, PSV_MAX_LONGITUDE;
  1340. if (min_long < max_long) {
  1341. PSV_MIN_LONGITUDE = min_long;
  1342. PSV_MAX_LONGITUDE = max_long;
  1343. }
  1344. else {
  1345. PSV_MIN_LONGITUDE = max_long;
  1346. PSV_MAX_LONGITUDE = min_long;
  1347. }
  1348. // Default position
  1349. var lat = 0, long = PSV_MIN_LONGITUDE;
  1350. if (args.default_position !== undefined) {
  1351. if (args.default_position.lat !== undefined) {
  1352. var lat_angle = parseAngle(args.default_position.lat);
  1353. if (lat_angle > Math.PI)
  1354. lat_angle -= 2 * Math.PI;
  1355. lat = stayBetween(lat_angle, PSV_TILT_DOWN_MAX, PSV_TILT_UP_MAX);
  1356. }
  1357. if (args.default_position.long !== undefined)
  1358. long = stayBetween(parseAngle(args.default_position.long), PSV_MIN_LONGITUDE, PSV_MAX_LONGITUDE);
  1359. }
  1360. // Sphere segments and rings
  1361. var segments = (args.segments !== undefined) ? parseInt(args.segments) : 100;
  1362. var rings = (args.rings !== undefined) ? parseInt(args.rings) : 100;
  1363. // Default zoom level
  1364. var zoom_lvl = 0;
  1365. if (args.zoom_level !== undefined)
  1366. zoom_lvl = stayBetween(parseInt(Math.round(args.zoom_level)), 0, 100);
  1367. var fov = PSV_FOV_MAX + (zoom_lvl / 100) * (PSV_FOV_MIN - PSV_FOV_MAX);
  1368. // Animation constants
  1369. var PSV_FRAMES_PER_SECOND = 60;
  1370. var PSV_ANIM_TIMEOUT = 1000 / PSV_FRAMES_PER_SECOND;
  1371. // Delay before the animation
  1372. var anim_delay = 2000;
  1373. if (args.time_anim !== undefined) {
  1374. if (typeof args.time_anim == 'number' && args.time_anim >= 0)
  1375. anim_delay = args.time_anim;
  1376. else
  1377. anim_delay = false;
  1378. }
  1379. // Horizontal animation speed
  1380. var anim_long_offset = (args.anim_speed !== undefined) ? parseAnimationSpeed(args.anim_speed) : parseAnimationSpeed('2rpm');
  1381. // Reverse the horizontal animation if autorotate reaches the min/max longitude
  1382. var reverse_anim = true;
  1383. if (args.reverse_anim !== undefined)
  1384. reverse_anim = !!args.reverse_anim;
  1385. // Vertical animation speed
  1386. var anim_lat_offset = (args.vertical_anim_speed !== undefined) ? parseAnimationSpeed(args.vertical_anim_speed) : parseAnimationSpeed('2rpm');
  1387. // Vertical animation target (default: equator)
  1388. var anim_lat_target = 0;
  1389. if (args.vertical_anim_target !== undefined) {
  1390. var lat_target_angle = parseAngle(args.vertical_anim_target);
  1391. if (lat_target_angle > Math.PI)
  1392. lat_target_angle -= 2 * Math.PI;
  1393. anim_lat_target = stayBetween(lat_target_angle, PSV_TILT_DOWN_MAX, PSV_TILT_UP_MAX);
  1394. }
  1395. // Navigation bar
  1396. var navbar = new PSVNavBar(this);
  1397. // Must we display the navigation bar?
  1398. var display_navbar = (args.navbar !== undefined) ? !!args.navbar : false;
  1399. // Style of the navigation bar
  1400. var navbar_style = (args.navbar_style !== undefined) ? args.navbar_style : {};
  1401. // Are user interactions allowed?
  1402. var user_interactions_allowed = (args.allow_user_interactions !== undefined) ? !!args.allow_user_interactions : true;
  1403. if (!user_interactions_allowed)
  1404. display_navbar = false;
  1405. // Is "scroll to zoom" allowed?
  1406. var scroll_to_zoom = (args.allow_scroll_to_zoom !== undefined) ? !!args.allow_scroll_to_zoom : true;
  1407. // User's zoom speed
  1408. var zoom_speed = (args.zoom_speed !== undefined) ? parseFloat(args.zoom_speed) : 1.0;
  1409. // Eyes offset in VR mode
  1410. var eyes_offset = (args.eyes_offset !== undefined) ? parseFloat(args.eyes_offset) : 5;
  1411. // Container (ID to retrieve?)
  1412. var container = (typeof args.container == 'string') ? document.getElementById(args.container) : args.container;
  1413. // Size of the viewer
  1414. var viewer_size, new_viewer_size = {}, real_viewer_size = {};
  1415. if (args.size !== undefined)
  1416. setNewViewerSize(args.size);
  1417. // Some useful attributes
  1418. var panorama = args.panorama;
  1419. var root, canvas_container;
  1420. var renderer = null, scene = null, camera = null, stereo_effect = null;
  1421. var mousedown = false, mouse_x = 0, mouse_y = 0;
  1422. var touchzoom = false, touchzoom_dist = 0;
  1423. var autorotate_timeout = null, anim_timeout = null;
  1424. var sphoords = new Sphoords();
  1425. var actions = {};
  1426. // Do we have to read XMP data?
  1427. var readxmp = (args.usexmpdata !== undefined) ? !!args.usexmpdata : true;
  1428. // Can we use CORS?
  1429. var cors_anonymous = (args.cors_anonymous !== undefined) ? !!args.cors_anonymous : true;
  1430. // Cropped size?
  1431. var pano_size = {
  1432. full_width: null,
  1433. full_height: null,
  1434. cropped_width: null,
  1435. cropped_height: null,
  1436. cropped_x: null,
  1437. cropped_y: null
  1438. };
  1439. // The user defines the real size of the panorama
  1440. if (args.pano_size !== undefined) {
  1441. for (var attr in pano_size) {
  1442. if (args.pano_size[attr] !== undefined)
  1443. pano_size[attr] = parseInt(args.pano_size[attr]);
  1444. }
  1445. readxmp = false;
  1446. }
  1447. // Captured FOVs
  1448. var captured_view = {
  1449. horizontal_fov: 360,
  1450. vertical_fov: 180
  1451. };
  1452. if (args.captured_view !== undefined) {
  1453. for (var attr in captured_view) {
  1454. if (args.captured_view[attr] !== undefined)
  1455. captured_view[attr] = parseFloat(args.captured_view[attr]);
  1456. }
  1457. readxmp = false;
  1458. }
  1459. // Will we have to recalculate the coordinates?
  1460. var recalculate_coords = false;
  1461. // Loading message
  1462. var loading_msg = (args.loading_msg !== undefined) ? args.loading_msg.toString() : 'Loading…';
  1463. // Loading image
  1464. var loading_img = (args.loading_img !== undefined) ? args.loading_img.toString() : null;
  1465. // Loading HTML
  1466. var loading_html = (args.loading_html !== undefined) ? args.loading_html : null;
  1467. // Overlay
  1468. var overlay = null;
  1469. if (args.overlay !== undefined) {
  1470. // Image
  1471. if (args.overlay.image !== undefined) {
  1472. overlay = {
  1473. image: args.overlay.image,
  1474. position: {
  1475. x: 'left',
  1476. y: 'bottom'
  1477. }
  1478. };
  1479. // Image position
  1480. if (args.overlay.position !== undefined) {
  1481. if (args.overlay.position.x !== undefined && (args.overlay.position.x == 'left' || args.overlay.position.x == 'right'))
  1482. overlay.position.x = args.overlay.position.x;
  1483. if (args.overlay.position.y !== undefined && (args.overlay.position.y == 'top' || args.overlay.position.y == 'bottom'))
  1484. overlay.position.y = args.overlay.position.y;
  1485. }
  1486. // Image size
  1487. if (args.overlay.size !== undefined) {
  1488. // Default: keep the original size, or resize following the current ratio
  1489. overlay.size = {
  1490. width: (args.overlay.size.width !== undefined) ? args.overlay.size.width : 'auto',
  1491. height: (args.overlay.size.height !== undefined) ? args.overlay.size.height : 'auto'
  1492. };
  1493. }
  1494. }
  1495. }
  1496. // Function to call once panorama is ready?
  1497. var self = this;
  1498. if (args.onready !== undefined)
  1499. this.addAction('ready', args.onready);
  1500. // Go?
  1501. var autoload = (args.autoload !== undefined) ? !!args.autoload : true;
  1502. if (autoload)
  1503. this.load();
  1504. };
  1505. /**
  1506. * Represents the navigation bar.
  1507. * @class
  1508. * @param {PhotoSphereViewer} psv - A PhotoSphereViewer object
  1509. **/
  1510. var PSVNavBar = function(psv) {
  1511. /**
  1512. * Checks if a value exists in an array.
  1513. * @private
  1514. * @param {*} searched - The searched value
  1515. * @param {array} array - The array
  1516. * @return {boolean} `true` if the value exists in the array, `false` otherwise
  1517. **/
  1518. var inArray = function(searched, array) {
  1519. for (var i = 0, l = array.length; i < l; ++i) {
  1520. if (array[i] == searched)
  1521. return true;
  1522. }
  1523. return false;
  1524. };
  1525. /**
  1526. * Checks if a property is valid.
  1527. * @private
  1528. * @param {string} property - The property
  1529. * @param {*} value - The value to check
  1530. * @return {boolean} `true` if the value is valid, `false` otherwise
  1531. **/
  1532. var checkValue = function(property, value) {
  1533. return (
  1534. // Color
  1535. (
  1536. inArray(property, colors) && (typeof value == 'string') &&
  1537. (
  1538. value == 'transparent' ||
  1539. !!value.match(/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/) ||
  1540. !!value.match(/^rgb\((1?[0-9]{1,2}|2[0-4][0-9]|25[0-5])(,\s*(1?[0-9]{1,2}|2[0-4][0-9]|25[0-5])){2}\)$/) ||
  1541. !!value.match(/^rgba\(((1?[0-9]{1,2}|2[0-4][0-9]|25[0-5]),\s*){3}(0(\.[0-9]*)?|1)\)$/)
  1542. )
  1543. ) ||
  1544. // Number
  1545. (inArray(property, numbers) && !isNaN(parseFloat(value)) && isFinite(value) && value >= 0)
  1546. );
  1547. };
  1548. /**
  1549. * Sets the style.
  1550. * @public
  1551. * @param {object} new_style - The properties to change
  1552. * @param {string} [new_style.backgroundColor=rgba(61, 61, 61, 0.5)] - Navigation bar background color
  1553. * @param {string} [new_style.buttonsColor=rgba(255, 255, 255, 0.7)] - Buttons foreground color
  1554. * @param {string} [new_style.buttonsBackgroundColor=transparent] - Buttons background color
  1555. * @param {string} [new_style.activeButtonsBackgroundColor=rgba(255, 255, 255, 0.1)] - Active buttons background color
  1556. * @param {number} [new_style.buttonsHeight=20] - Buttons height in pixels
  1557. * @param {number} [new_style.autorotateThickness=1] - Autorotate icon thickness in pixels
  1558. * @param {number} [new_style.zoomRangeWidth=50] - Zoom range width in pixels
  1559. * @param {number} [new_style.zoomRangeThickness=1] - Zoom range thickness in pixels
  1560. * @param {number} [new_style.zoomRangeDisk=7] - Zoom range disk diameter in pixels
  1561. * @param {number} [new_style.fullscreenRatio=4/3] - Fullscreen icon ratio (width / height)
  1562. * @param {number} [new_style.fullscreenThickness=2] - Fullscreen icon thickness in pixels
  1563. * @param {number} [new_style.gyroscopeThickness=1] - Gyroscope icon thickness in pixels
  1564. * @param {number} [new_style.virtualRealityRatio=4/3] - Virtual reality icon ratio (width / height)
  1565. * @param {number} [new_style.virtualRealityBorderRadius=2] - Virtual reality icon border radius in pixels
  1566. * @return {void}
  1567. **/
  1568. this.setStyle = function(new_style) {
  1569. // Properties to change
  1570. for (var property in new_style) {
  1571. // Is this property a property we'll use?
  1572. if ((property in style) && checkValue(property, new_style[property]))
  1573. style[property] = new_style[property];
  1574. }
  1575. };
  1576. /**
  1577. * Creates the elements.
  1578. * @public
  1579. * @return {void}
  1580. **/
  1581. this.create = function() {
  1582. // Container
  1583. container = document.createElement('div');
  1584. container.style.backgroundColor = style.backgroundColor;
  1585. container.style.position = 'absolute';
  1586. container.style.zIndex = 10;
  1587. container.style.bottom = 0;
  1588. container.style.width = '100%';
  1589. container.style.boxSizing = 'content-box';
  1590. container.style.transition = 'bottom 0.4s ease-out';
  1591. // Autorotate button
  1592. autorotate = new PSVNavBarButton(psv, 'autorotate', style);
  1593. container.appendChild(autorotate.getButton());
  1594. // Zoom buttons
  1595. zoom = new PSVNavBarButton(psv, 'zoom', style);
  1596. container.appendChild(zoom.getButton());
  1597. // Fullscreen button
  1598. fullscreen = new PSVNavBarButton(psv, 'fullscreen', style);
  1599. container.appendChild(fullscreen.getButton());
  1600. if (Sphoords.isDeviceOrientationSupported) {
  1601. // Device orientation button
  1602. orientation = new PSVNavBarButton(psv, 'orientation', style);
  1603. container.appendChild(orientation.getButton());
  1604. // Virtual reality button
  1605. vr = new PSVNavBarButton(psv, 'virtual-reality', style);
  1606. container.appendChild(vr.getButton());
  1607. }
  1608. };
  1609. /**
  1610. * Returns the bar itself.
  1611. * @public
  1612. * @return {HTMLElement} The bar
  1613. **/
  1614. this.getBar = function() {
  1615. return container;
  1616. };
  1617. /**
  1618. * Shows the bar.
  1619. * @private
  1620. * @return {void}
  1621. **/
  1622. var show = function() {
  1623. // Stop hiding the bar if necessary
  1624. if (!!must_hide_timeout) {
  1625. clearTimeout(must_hide_timeout);
  1626. if (!hidden && must_be_hidden)
  1627. must_hide_timeout = setTimeout(hide, 5000);
  1628. }
  1629. if (hidden) {
  1630. container.style.bottom = 0;
  1631. hidden = false;
  1632. // If bar must be hidden, we hide it again
  1633. if (must_be_hidden)
  1634. must_hide_timeout = setTimeout(hide, 5000);
  1635. }
  1636. };
  1637. /**
  1638. * Shows the bar.
  1639. * @public
  1640. * @return {void}
  1641. **/
  1642. this.show = function() {
  1643. show();
  1644. };
  1645. /**
  1646. * Hides the bar.
  1647. * @private
  1648. * @return {void}
  1649. **/
  1650. var hide = function() {
  1651. if (!hidden) {
  1652. container.style.bottom = (-container.offsetHeight + 1) + 'px';
  1653. hidden = true;
  1654. }
  1655. };
  1656. /**
  1657. * Hides the bar.
  1658. * @public
  1659. * @return {void}
  1660. **/
  1661. this.hide = function() {
  1662. hide();
  1663. };
  1664. /**
  1665. * Returns the current state.
  1666. * @public
  1667. * @return {boolean} `true` if navigation bar is hidden, `false` otherwise
  1668. **/
  1669. this.isHidden = function() {
  1670. return hidden;
  1671. };
  1672. /**
  1673. * Indicates that the bar must be hidden or not.
  1674. * @public
  1675. * @param {boolean} [state=true] - `true` to automatically hide the bar, `false` to show it
  1676. * @return {void}
  1677. **/
  1678. this.mustBeHidden = function(state) {
  1679. must_be_hidden = (state !== undefined) ? !!state : true;
  1680. if (must_be_hidden)
  1681. hide();
  1682. else
  1683. show();
  1684. };
  1685. // Default style
  1686. var style = {
  1687. // Bar background
  1688. backgroundColor: 'rgba(61, 61, 61, 0.5)',
  1689. // Buttons foreground color
  1690. buttonsColor: 'rgba(255, 255, 255, 0.7)',
  1691. // Buttons background color
  1692. buttonsBackgroundColor: 'transparent',
  1693. // Buttons background color when active
  1694. activeButtonsBackgroundColor: 'rgba(255, 255, 255, 0.1)',
  1695. // Buttons height in pixels
  1696. buttonsHeight: 20,
  1697. // Autorotate icon thickness in pixels
  1698. autorotateThickness: 1,
  1699. // Zoom range width in pixels
  1700. zoomRangeWidth: 50,
  1701. // Zoom range thickness in pixels
  1702. zoomRangeThickness: 1,
  1703. // Zoom range disk diameter in pixels
  1704. zoomRangeDisk: 7,
  1705. // Fullscreen icon ratio
  1706. fullscreenRatio: 4 / 3,
  1707. // Fullscreen icon thickness in pixels
  1708. fullscreenThickness: 2,
  1709. // Gyroscope icon thickness in pixels
  1710. gyroscopeThickness: 1,
  1711. // Virtual reality icon ratio
  1712. virtualRealityRatio: 4 / 3,
  1713. // Virtual reality icon border radius in pixels
  1714. virtualRealityBorderRadius: 2
  1715. };
  1716. // Properties types
  1717. var colors = ['backgroundColor', 'buttonsColor', 'buttonsBackgroundColor', 'activeButtonsBackgroundColor'];
  1718. var numbers = ['buttonsHeight', 'autorotateThickness', 'zoomRangeWidth', 'zoomRangeThickness', 'zoomRangeDisk', 'fullscreenRatio', 'fullscreenThickness'];
  1719. // Some useful attributes
  1720. var container;
  1721. var autorotate, zoom, fullscreen, orientation, vr;
  1722. var must_hide_timeout = null;
  1723. var hidden = false, must_be_hidden = false;
  1724. };
  1725. /**
  1726. * Represents a navigation bar button.
  1727. * @class
  1728. * @param {PhotoSphereViewer} psv - A PhotoSphereViewer object
  1729. * @param {string} type - Type of button
  1730. * @param {object} style - Style of the navigation bar
  1731. **/
  1732. var PSVNavBarButton = function(psv, type, style) {
  1733. /**
  1734. * Attaches an event handler function to an elemnt.
  1735. * @private
  1736. * @param {HTMLElement} elt - The element
  1737. * @param {string} evt - The event name
  1738. * @param {Function} f - The handler function
  1739. * @return {void}
  1740. **/
  1741. var addEvent = function(elt, evt, f) {
  1742. if (!!elt.addEventListener)
  1743. elt.addEventListener(evt, f, false);
  1744. else
  1745. elt.attachEvent('on' + evt, f);
  1746. };
  1747. /**
  1748. * Creates the right button.
  1749. * @private
  1750. * @return {void}
  1751. **/
  1752. var create = function() {
  1753. switch (type) {
  1754. case 'autorotate':
  1755. // Autorotate icon sizes
  1756. var autorotate_sphere_width = style.buttonsHeight - style.autorotateThickness * 2;
  1757. var autorotate_equator_height = autorotate_sphere_width / 10;
  1758. // Autorotate button
  1759. button = document.createElement('div');
  1760. button.style.cssFloat = 'left';
  1761. button.style.boxSizing = 'inherit';
  1762. button.style.padding = '10px';
  1763. button.style.width = style.buttonsHeight + 'px';
  1764. button.style.height = style.buttonsHeight + 'px';
  1765. button.style.backgroundColor = style.buttonsBackgroundColor;
  1766. button.style.position = 'relative';
  1767. button.style.cursor = 'pointer';
  1768. addEvent(button, 'click', function(){psv.toggleAutorotate();});
  1769. var autorotate_sphere = document.createElement('div');
  1770. autorotate_sphere.style.boxSizing = 'inherit';
  1771. autorotate_sphere.style.width = autorotate_sphere_width + 'px';
  1772. autorotate_sphere.style.height = autorotate_sphere_width + 'px';
  1773. autorotate_sphere.style.borderRadius = '50%';
  1774. autorotate_sphere.style.border = style.autorotateThickness + 'px solid ' + style.buttonsColor;
  1775. button.appendChild(autorotate_sphere);
  1776. var autorotate_equator = document.createElement('div');
  1777. autorotate_equator.style.boxSizing = 'inherit';
  1778. autorotate_equator.style.width = autorotate_sphere_width + 'px';
  1779. autorotate_equator.style.height = autorotate_equator_height + 'px';
  1780. autorotate_equator.style.borderRadius = '50%';
  1781. autorotate_equator.style.border = style.autorotateThickness + 'px solid ' + style.buttonsColor;
  1782. autorotate_equator.style.position = 'absolute';
  1783. autorotate_equator.style.top = '50%';
  1784. autorotate_equator.style.marginTop = -(autorotate_equator_height / 2 + style.autorotateThickness) + 'px';
  1785. button.appendChild(autorotate_equator);
  1786. // (In)active
  1787. psv.addAction('autorotate', toggleActive);
  1788. break;
  1789. case 'zoom':
  1790. // Zoom container
  1791. button = document.createElement('div');
  1792. button.style.cssFloat = 'left';
  1793. button.style.boxSizing = 'inherit';
  1794. // Zoom "-"
  1795. var zoom_minus = document.createElement('div');
  1796. zoom_minus.style.cssFloat = 'left';
  1797. zoom_minus.style.boxSizing = 'inherit';
  1798. zoom_minus.style.padding = '10px';
  1799. zoom_minus.style.height = style.buttonsHeight + 'px';
  1800. zoom_minus.style.backgroundColor = style.buttonsBackgroundColor;
  1801. zoom_minus.style.lineHeight = style.buttonsHeight + 'px';
  1802. zoom_minus.style.color = style.buttonsColor;
  1803. zoom_minus.style.cursor = 'pointer';
  1804. zoom_minus.textContent = '-';
  1805. addEvent(zoom_minus, 'click', function(){psv.zoomOut();});
  1806. button.appendChild(zoom_minus);
  1807. // Zoom range
  1808. zoom_range_bg = document.createElement('div');
  1809. zoom_range_bg.style.cssFloat = 'left';
  1810. zoom_range_bg.style.boxSizing = 'inherit';
  1811. zoom_range_bg.style.padding = (10 + (style.buttonsHeight - style.zoomRangeThickness) / 2) + 'px 5px';
  1812. zoom_range_bg.style.backgroundColor = style.buttonsBackgroundColor;
  1813. zoom_range_bg.style.cursor = 'pointer';
  1814. button.appendChild(zoom_range_bg);
  1815. zoom_range = document.createElement('div');
  1816. zoom_range.style.boxSizing = 'inherit';
  1817. zoom_range.style.width = style.zoomRangeWidth + 'px';
  1818. zoom_range.style.height = style.zoomRangeThickness + 'px';
  1819. zoom_range.style.backgroundColor = style.buttonsColor;
  1820. zoom_range.style.position = 'relative';
  1821. zoom_range_bg.appendChild(zoom_range);
  1822. zoom_value = document.createElement('div');
  1823. zoom_value.style.position = 'absolute';
  1824. zoom_value.style.top = ((style.zoomRangeThickness - style.zoomRangeDisk) / 2) + 'px';
  1825. zoom_value.style.left = -(style.zoomRangeDisk / 2) + 'px';
  1826. zoom_value.style.boxSizing = 'inherit';
  1827. zoom_value.style.width = style.zoomRangeDisk + 'px';
  1828. zoom_value.style.height = style.zoomRangeDisk + 'px';
  1829. zoom_value.style.borderRadius = '50%';
  1830. zoom_value.style.backgroundColor = style.buttonsColor;
  1831. psv.addAction('zoom-updated', moveZoomValue);
  1832. addEvent(zoom_range_bg, 'mousedown', initZoomChangeWithMouse);
  1833. addEvent(zoom_range_bg, 'touchstart', initZoomChangeByTouch);
  1834. addEvent(document, 'mousemove', changeZoomWithMouse);
  1835. addEvent(document, 'touchmove', changeZoomByTouch);
  1836. addEvent(document, 'mouseup', stopZoomChange);
  1837. addEvent(document, 'touchend', stopZoomChange);
  1838. addEvent(zoom_range_bg, 'mousewheel', changeZoomOnMouseWheel);
  1839. addEvent(zoom_range_bg, 'DOMMouseScroll', changeZoomOnMouseWheel);
  1840. zoom_range.appendChild(zoom_value);
  1841. // Zoom "+"
  1842. var zoom_plus = document.createElement('div');
  1843. zoom_plus.style.cssFloat = 'left';
  1844. zoom_plus.style.boxSizing = 'inherit';
  1845. zoom_plus.style.padding = '10px';
  1846. zoom_plus.style.height = style.buttonsHeight + 'px';
  1847. zoom_plus.style.backgroundColor = style.buttonsBackgroundColor;
  1848. zoom_plus.style.lineHeight = style.buttonsHeight + 'px';
  1849. zoom_plus.style.color = style.buttonsColor;
  1850. zoom_plus.style.cursor = 'pointer';
  1851. zoom_plus.textContent = '+';
  1852. addEvent(zoom_plus, 'click', function(){psv.zoomIn();});
  1853. button.appendChild(zoom_plus);
  1854. break;
  1855. case 'fullscreen':
  1856. // Fullscreen icon size
  1857. var fullscreen_width = style.buttonsHeight * style.fullscreenRatio;
  1858. var fullscreen_vertical_space = style.buttonsHeight * 0.3;
  1859. var fullscreen_vertical_border = (style.buttonsHeight - fullscreen_vertical_space) / 2;
  1860. var fullscreen_horizontal_space = fullscreen_width * 0.3;
  1861. var fullscreen_horizontal_border = (fullscreen_width - fullscreen_horizontal_space) / 2 - style.fullscreenThickness;
  1862. var fullscreen_vertical_int = style.buttonsHeight - style.fullscreenThickness * 2;
  1863. // Fullscreen button
  1864. button = document.createElement('div');
  1865. button.style.cssFloat = 'right';
  1866. button.style.boxSizing = 'inherit';
  1867. button.style.padding = '10px';
  1868. button.style.width = fullscreen_width + 'px';
  1869. button.style.height = style.buttonsHeight + 'px';
  1870. button.style.backgroundColor = style.buttonsBackgroundColor;
  1871. button.style.cursor = 'pointer';
  1872. addEvent(button, 'click', function(){psv.toggleFullscreen();});
  1873. // Fullscreen icon left side
  1874. var fullscreen_left = document.createElement('div');
  1875. fullscreen_left.style.cssFloat = 'left';
  1876. fullscreen_left.style.boxSizing = 'inherit';
  1877. fullscreen_left.style.width = style.fullscreenThickness + 'px';
  1878. fullscreen_left.style.height = fullscreen_vertical_space + 'px';
  1879. fullscreen_left.style.borderStyle = 'solid';
  1880. fullscreen_left.style.borderColor = style.buttonsColor + ' transparent';
  1881. fullscreen_left.style.borderWidth = fullscreen_vertical_border + 'px 0';
  1882. button.appendChild(fullscreen_left);
  1883. // Fullscreen icon top/bottom sides (first half)
  1884. var fullscreen_tb_1 = document.createElement('div');
  1885. fullscreen_tb_1.style.cssFloat = 'left';
  1886. fullscreen_tb_1.style.boxSizing = 'inherit';
  1887. fullscreen_tb_1.style.width = fullscreen_horizontal_border + 'px';
  1888. fullscreen_tb_1.style.height = fullscreen_vertical_int + 'px';
  1889. fullscreen_tb_1.style.borderStyle = 'solid';
  1890. fullscreen_tb_1.style.borderColor = style.buttonsColor + ' transparent';
  1891. fullscreen_tb_1.style.borderWidth = style.fullscreenThickness + 'px 0';
  1892. button.appendChild(fullscreen_tb_1);
  1893. // Fullscreen icon top/bottom sides (second half)
  1894. var fullscreen_tb_2 = document.createElement('div');
  1895. fullscreen_tb_2.style.cssFloat = 'left';
  1896. fullscreen_tb_2.style.boxSizing = 'inherit';
  1897. fullscreen_tb_2.style.marginLeft = fullscreen_horizontal_space + 'px';
  1898. fullscreen_tb_2.style.width = fullscreen_horizontal_border + 'px';
  1899. fullscreen_tb_2.style.height = fullscreen_vertical_int + 'px';
  1900. fullscreen_tb_2.style.borderStyle = 'solid';
  1901. fullscreen_tb_2.style.borderColor = style.buttonsColor + ' transparent';
  1902. fullscreen_tb_2.style.borderWidth = style.fullscreenThickness + 'px 0';
  1903. button.appendChild(fullscreen_tb_2);
  1904. // Fullscreen icon right side
  1905. var fullscreen_right = document.createElement('div');
  1906. fullscreen_right.style.cssFloat = 'left';
  1907. fullscreen_right.style.boxSizing = 'inherit';
  1908. fullscreen_right.style.width = style.fullscreenThickness + 'px';
  1909. fullscreen_right.style.height = fullscreen_vertical_space + 'px';
  1910. fullscreen_right.style.borderStyle = 'solid';
  1911. fullscreen_right.style.borderColor = style.buttonsColor + ' transparent';
  1912. fullscreen_right.style.borderWidth = fullscreen_vertical_border + 'px 0';
  1913. button.appendChild(fullscreen_right);
  1914. var fullscreen_clearer = document.createElement('div');
  1915. fullscreen_clearer.style.clear = 'left';
  1916. button.appendChild(fullscreen_clearer);
  1917. // (In)active
  1918. psv.addAction('fullscreen-mode', toggleActive);
  1919. break;
  1920. case 'orientation':
  1921. // Gyroscope icon sizes
  1922. var gyroscope_sphere_width = style.buttonsHeight - style.gyroscopeThickness * 2;
  1923. var gyroscope_ellipses_big_axis = gyroscope_sphere_width - style.gyroscopeThickness * 4;
  1924. var gyroscope_ellipses_little_axis = gyroscope_sphere_width / 10;
  1925. // Gyroscope button
  1926. button = document.createElement('div');
  1927. button.style.cssFloat = 'right';
  1928. button.style.boxSizing = 'inherit';
  1929. button.style.padding = '10px';
  1930. button.style.width = style.buttonsHeight + 'px';
  1931. button.style.height = style.buttonsHeight + 'px';
  1932. button.style.backgroundColor = style.buttonsBackgroundColor;
  1933. button.style.position = 'relative';
  1934. button.style.cursor = 'pointer';
  1935. addEvent(button, 'click', function(){psv.toggleDeviceOrientation();});
  1936. var gyroscope_sphere = document.createElement('div');
  1937. gyroscope_sphere.style.boxSizing = 'inherit';
  1938. gyroscope_sphere.style.width = gyroscope_sphere_width + 'px';
  1939. gyroscope_sphere.style.height = gyroscope_sphere_width + 'px';
  1940. gyroscope_sphere.style.borderRadius = '50%';
  1941. gyroscope_sphere.style.border = style.gyroscopeThickness + 'px solid ' + style.buttonsColor;
  1942. button.appendChild(gyroscope_sphere);
  1943. var gyroscope_hor_ellipsis = document.createElement('div');
  1944. gyroscope_hor_ellipsis.style.boxSizing = 'inherit';
  1945. gyroscope_hor_ellipsis.style.width = gyroscope_ellipses_big_axis + 'px';
  1946. gyroscope_hor_ellipsis.style.height = gyroscope_ellipses_little_axis + 'px';
  1947. gyroscope_hor_ellipsis.style.borderRadius = '50%';
  1948. gyroscope_hor_ellipsis.style.border = style.gyroscopeThickness + 'px solid ' + style.buttonsColor;
  1949. gyroscope_hor_ellipsis.style.position = 'absolute';
  1950. gyroscope_hor_ellipsis.style.top = '50%';
  1951. gyroscope_hor_ellipsis.style.left = '50%';
  1952. gyroscope_hor_ellipsis.style.marginTop = -(gyroscope_ellipses_little_axis / 2 + style.gyroscopeThickness) + 'px';
  1953. gyroscope_hor_ellipsis.style.marginLeft = -(gyroscope_ellipses_big_axis / 2 + style.gyroscopeThickness) + 'px';
  1954. button.appendChild(gyroscope_hor_ellipsis);
  1955. var gyroscope_ver_ellipsis = document.createElement('div');
  1956. gyroscope_ver_ellipsis.style.boxSizing = 'inherit';
  1957. gyroscope_ver_ellipsis.style.width = gyroscope_ellipses_little_axis + 'px';
  1958. gyroscope_ver_ellipsis.style.height = gyroscope_ellipses_big_axis + 'px';
  1959. gyroscope_ver_ellipsis.style.borderRadius = '50%';
  1960. gyroscope_ver_ellipsis.style.border = style.gyroscopeThickness + 'px solid ' + style.buttonsColor;
  1961. gyroscope_ver_ellipsis.style.position = 'absolute';
  1962. gyroscope_ver_ellipsis.style.top = '50%';
  1963. gyroscope_ver_ellipsis.style.left = '50%';
  1964. gyroscope_ver_ellipsis.style.marginTop = -(gyroscope_ellipses_big_axis / 2 + style.gyroscopeThickness) + 'px';
  1965. gyroscope_ver_ellipsis.style.marginLeft = -(gyroscope_ellipses_little_axis / 2 + style.gyroscopeThickness) + 'px';
  1966. button.appendChild(gyroscope_ver_ellipsis);
  1967. // (In)active
  1968. psv.addAction('device-orientation', toggleActive);
  1969. break;
  1970. case 'virtual-reality':
  1971. // Sizes
  1972. var vr_width = style.buttonsHeight * style.virtualRealityRatio;
  1973. var vr_eye_diameter = vr_width / 4;
  1974. var vr_eye_offset = vr_eye_diameter / 2;
  1975. // Button
  1976. button = document.createElement('div');
  1977. button.style.cssFloat = 'right';
  1978. button.style.position = 'relative';
  1979. button.style.boxSizing = 'inherit';
  1980. button.style.padding = '10px';
  1981. button.style.width = vr_width + 'px';
  1982. button.style.height = style.buttonsHeight + 'px';
  1983. button.style.backgroundColor = style.buttonsBackgroundColor;
  1984. button.style.cursor = 'pointer';
  1985. addEvent(button, 'click', function(){psv.toggleStereo();});
  1986. // Icon
  1987. var vr_rect = document.createElement('div');
  1988. vr_rect.style.boxSizing = 'inherit';
  1989. vr_rect.style.width = vr_width + 'px';
  1990. vr_rect.style.height = style.buttonsHeight + 'px';
  1991. vr_rect.style.borderRadius = style.virtualRealityBorderRadius + 'px';
  1992. vr_rect.style.backgroundColor = style.buttonsColor;
  1993. button.appendChild(vr_rect);
  1994. var left_eye = document.createElement('div');
  1995. left_eye.style.boxSizing = 'inherit';
  1996. left_eye.style.width = vr_eye_diameter + 'px';
  1997. left_eye.style.height = vr_eye_diameter + 'px';
  1998. left_eye.style.position = 'absolute';
  1999. left_eye.style.top = (vr_eye_offset + 10) + 'px';
  2000. left_eye.style.left = (vr_eye_offset + 10) + 'px';
  2001. left_eye.style.borderRadius = '50%';
  2002. left_eye.style.backgroundColor = style.backgroundColor;
  2003. button.appendChild(left_eye);
  2004. var right_eye = document.createElement('div');
  2005. right_eye.style.boxSizing = 'inherit';
  2006. right_eye.style.width = vr_eye_diameter + 'px';
  2007. right_eye.style.height = vr_eye_diameter + 'px';
  2008. right_eye.style.position = 'absolute';
  2009. right_eye.style.top = (vr_eye_offset + 10) + 'px';
  2010. right_eye.style.right = (vr_eye_offset + 10) + 'px';
  2011. right_eye.style.borderRadius = '50%';
  2012. right_eye.style.backgroundColor = style.backgroundColor;
  2013. button.appendChild(right_eye);
  2014. var nose = document.createElement('div');
  2015. nose.style.boxSizing = 'inherit';
  2016. nose.style.width = vr_eye_diameter + 'px';
  2017. nose.style.height = (style.buttonsHeight / 2) + 'px';
  2018. nose.style.position = 'absolute';
  2019. nose.style.left = '50%';
  2020. nose.style.bottom = '10px';
  2021. nose.style.marginLeft = -(vr_eye_diameter / 2) + 'px';
  2022. nose.style.borderTopLeftRadius = '50% 60%';
  2023. nose.style.borderTopRightRadius = '50% 60%';
  2024. nose.style.backgroundColor = style.backgroundColor;
  2025. button.appendChild(nose);
  2026. //(In)active
  2027. psv.addAction('stereo-effect', toggleActive);
  2028. break;
  2029. }
  2030. };
  2031. /**
  2032. * Returns the button element.
  2033. * @public
  2034. * @return {HTMLElement} The button
  2035. **/
  2036. this.getButton = function() {
  2037. return button;
  2038. };
  2039. /**
  2040. * Changes the active state of the button.
  2041. * @private
  2042. * @param {boolean} active - `true` if the button should be active, `false` otherwise
  2043. * @return {void}
  2044. **/
  2045. var toggleActive = function(active) {
  2046. if (active)
  2047. button.style.backgroundColor = style.activeButtonsBackgroundColor;
  2048. else
  2049. button.style.backgroundColor = style.buttonsBackgroundColor;
  2050. };
  2051. /**
  2052. * Moves the zoom cursor.
  2053. * @private
  2054. * @param {integer} level - Zoom level (between 0 and 100)
  2055. * @return {void}
  2056. **/
  2057. var moveZoomValue = function(level) {
  2058. zoom_value.style.left = (level / 100 * style.zoomRangeWidth - style.zoomRangeDisk / 2) + 'px';
  2059. };
  2060. /**
  2061. * The user wants to zoom.
  2062. * @private
  2063. * @param {Event} evt - The event
  2064. * @return {void}
  2065. **/
  2066. var initZoomChangeWithMouse = function(evt) {
  2067. initZoomChange(parseInt(evt.clientX));
  2068. };
  2069. /**
  2070. * The user wants to zoom (mobile version).
  2071. * @private
  2072. * @param {Event} evt - The event
  2073. * @return {void}
  2074. **/
  2075. var initZoomChangeByTouch = function(evt) {
  2076. var touch = evt.touches[0];
  2077. if (touch.target == zoom_range_bg || touch.target == zoom_range || touch.target == zoom_value)
  2078. initZoomChange(parseInt(touch.clientX));
  2079. };
  2080. /**
  2081. * Initializes a zoom change.
  2082. * @private
  2083. * @param {integer} x - Horizontal coordinate
  2084. * @return {void}
  2085. **/
  2086. var initZoomChange = function(x) {
  2087. mousedown = true;
  2088. changeZoom(x);
  2089. };
  2090. /**
  2091. * The user wants to stop zooming.
  2092. * @private
  2093. * @param {Event} evt - The event
  2094. * @return {void}
  2095. **/
  2096. var stopZoomChange = function(evt) {
  2097. mousedown = false;
  2098. };
  2099. /**
  2100. * The user moves the zoom cursor.
  2101. * @private
  2102. * @param {Event} evt - The event
  2103. * @return {void}
  2104. **/
  2105. var changeZoomWithMouse = function(evt) {
  2106. // evt.preventDefault();
  2107. changeZoom(parseInt(evt.clientX));
  2108. };
  2109. /**
  2110. * The user moves the zoom cursor (mobile version).
  2111. * @private
  2112. * @param {Event} evt - The event
  2113. * @return {void}
  2114. **/
  2115. var changeZoomByTouch = function(evt) {
  2116. var touch = evt.touches[0];
  2117. if (touch.target == zoom_range_bg || touch.target == zoom_range || touch.target == zoom_value) {
  2118. evt.preventDefault();
  2119. changeZoom(parseInt(touch.clientX));
  2120. }
  2121. };
  2122. /**
  2123. * Zoom change.
  2124. * @private
  2125. * @param {integer} x - Horizontal coordinate
  2126. * @return {void}
  2127. **/
  2128. var changeZoom = function(x) {
  2129. if (mousedown) {
  2130. var user_input = x - zoom_range.getBoundingClientRect().left;
  2131. var zoom_level = user_input / style.zoomRangeWidth * 100;
  2132. psv.zoom(zoom_level);
  2133. }
  2134. };
  2135. /**
  2136. * Change zoom by scrolling.
  2137. * @private
  2138. * @param {Event} evt - The event
  2139. * @return {void}
  2140. **/
  2141. var changeZoomOnMouseWheel = function(evt) {
  2142. psv.mouseWheel(evt);
  2143. };
  2144. // Some useful attributes
  2145. var zoom_range_bg, zoom_range, zoom_value;
  2146. var mousedown = false;
  2147. // Create the button
  2148. var button;
  2149. create();
  2150. };
  2151. /*
  2152. * Sphoords v0.1.1
  2153. * http://jeremyheleine.me
  2154. *
  2155. * Copyright (c) 2015,2016 Jérémy Heleine
  2156. *
  2157. * Permission is hereby granted, free of charge, to any person obtaining a copy
  2158. * of this software and associated documentation files (the "Software"), to deal
  2159. * in the Software without restriction, including without limitation the rights
  2160. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  2161. * copies of the Software, and to permit persons to whom the Software is
  2162. * furnished to do so, subject to the following conditions:
  2163. *
  2164. * The above copyright notice and this permission notice shall be included in
  2165. * all copies or substantial portions of the Software.
  2166. *
  2167. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  2168. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  2169. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  2170. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  2171. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  2172. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  2173. * THE SOFTWARE.
  2174. */
  2175. /**
  2176. * Sphoords class allowing to retrieve the current orientation of a device supporting the Orientation API.
  2177. * @class
  2178. **/
  2179. var Sphoords = function() {
  2180. /**
  2181. * Detects the used browser engine.
  2182. * @private
  2183. * @return {void}
  2184. **/
  2185. var detectBrowserEngine = function() {
  2186. // User-Agent
  2187. var ua = navigator.userAgent;
  2188. // Gecko
  2189. if (/Gecko\/[0-9.]+/.test(ua))
  2190. return 'Gecko';
  2191. // Blink
  2192. if (/Chrome\/[0-9.]+/.test(ua))
  2193. return 'Blink';
  2194. // WebKit
  2195. if (/AppleWebKit\/[0-9.]+/.test(ua))
  2196. return 'WebKit';
  2197. // Trident
  2198. if (/Trident\/[0-9.]+/.test(ua))
  2199. return 'Trident';
  2200. // Presto
  2201. if (/Opera\/[0-9.]+/.test(ua))
  2202. return 'Presto';
  2203. // No engine detected, Gecko by default
  2204. return 'Gecko';
  2205. };
  2206. /**
  2207. * Returns the principal angle in degrees.
  2208. * @private
  2209. * @param {number} angle - The initial angle
  2210. * @return {number} The principal angle
  2211. **/
  2212. var getPrincipalAngle = function(angle) {
  2213. return angle - Math.floor(angle / 360.0) * 360.0;
  2214. };
  2215. /**
  2216. * Attaches the DeviceOrientation event to the window and starts the record, only if Device Orientation is supported.
  2217. * @public
  2218. * @return {boolean} `true` if event is attached, `false` otherwise
  2219. **/
  2220. this.start = function() {
  2221. if (Sphoords.isDeviceOrientationSupported) {
  2222. window.addEventListener('deviceorientation', onDeviceOrientation, false);
  2223. recording = true;
  2224. return true;
  2225. }
  2226. else {
  2227. console.log('Device Orientation API not supported');
  2228. return false;
  2229. }
  2230. };
  2231. /**
  2232. * Stops the record by removing the event handler.
  2233. * @public
  2234. * @return {void}
  2235. **/
  2236. this.stop = function() {
  2237. // Is there something to remove?
  2238. if (recording) {
  2239. window.removeEventListener('deviceorientation', onDeviceOrientation, false);
  2240. recording = false;
  2241. }
  2242. };
  2243. /**
  2244. * Toggles the recording state.
  2245. * @public
  2246. * @return {void}
  2247. **/
  2248. this.toggle = function() {
  2249. if (recording)
  2250. this.stop();
  2251. else
  2252. this.start();
  2253. };
  2254. /**
  2255. * Determines whether Device Orientation Event is attached.
  2256. * @public
  2257. * @return {boolean} `true` if event is attached, `false` otherwise
  2258. **/
  2259. this.isEventAttached = function() {
  2260. return recording;
  2261. };
  2262. /**
  2263. * Records the current orientation
  2264. * @private
  2265. * @param {Event} evt - The event
  2266. * @return {void}
  2267. **/
  2268. var onDeviceOrientation = function(evt) {
  2269. // Current screen orientation
  2270. orientation = Sphoords.getScreenOrientation();
  2271. // Coordinates depend on the orientation
  2272. var theta = 0, phi = 0;
  2273. switch (orientation) {
  2274. // Portrait mode
  2275. case 'portrait-primary':
  2276. theta = evt.alpha + evt.gamma;
  2277. phi = evt.beta - 90;
  2278. break;
  2279. // Landscape mode
  2280. case 'landscape-primary':
  2281. // If "-90" is not present for theta, origin won't be the same than for portrait mode
  2282. theta = evt.alpha + evt.beta - 90;
  2283. phi = -evt.gamma - 90;
  2284. // The user looks to the top while phi "looks" to the bottom
  2285. if (Math.abs(evt.beta) > 90) {
  2286. // Here browser engines have different behaviors
  2287. // Hope we have a really respected standard soon
  2288. switch (engine) {
  2289. case 'Blink':
  2290. phi += 180;
  2291. break;
  2292. case 'Gecko':
  2293. default:
  2294. phi = -phi;
  2295. break;
  2296. }
  2297. }
  2298. //fix to work on iOS (tested on Safari and Chrome)
  2299. if( engine === 'WebKit' && !!window.orientation ){
  2300. if( phi < 0 ){
  2301. phi = (phi + 180) * -1;
  2302. }
  2303. if( theta >= 180 ){
  2304. theta = theta - 180;
  2305. } else {
  2306. theta = theta + 180;
  2307. }
  2308. }
  2309. break;
  2310. // Landscape mode (inversed)
  2311. case 'landscape-secondary':
  2312. // Still the same reason for "+90"
  2313. theta = evt.alpha - evt.beta + 90;
  2314. phi = evt.gamma - 90;
  2315. // The user looks to the top while phi "looks" to the bottom
  2316. if (Math.abs(evt.beta) > 90) {
  2317. // Here again, some different behaviors…
  2318. switch (engine) {
  2319. case 'Blink':
  2320. phi += 180;
  2321. break;
  2322. case 'Gecko':
  2323. default:
  2324. phi = -phi;
  2325. break;
  2326. }
  2327. }
  2328. //fix to work on iOS (tested on Safari and Chrome)
  2329. if( engine === 'WebKit' && !!window.orientation ){
  2330. if( phi < 0 ){
  2331. phi = (phi + 180) * -1;
  2332. }
  2333. if( theta >= 180 ){
  2334. theta = theta - 180;
  2335. } else {
  2336. theta = theta + 180;
  2337. }
  2338. }
  2339. break;
  2340. // Portrait mode (inversed)
  2341. case 'portrait-secondary':
  2342. theta = evt.alpha - evt.gamma;
  2343. phi = 180 - (evt.beta - 90);
  2344. phi = 270 - evt.beta;
  2345. break;
  2346. }
  2347. // First, we want phi to be between -π and π
  2348. phi = getPrincipalAngle(phi);
  2349. if (phi >= 180)
  2350. phi -= 360;
  2351. // We store the right values
  2352. long_deg = getPrincipalAngle(theta);
  2353. lat_deg = Math.max(-90, Math.min(90, phi));
  2354. long = long_deg * DEG_TO_RAD;
  2355. lat = lat_deg * DEG_TO_RAD;
  2356. // We execute the wanted functions
  2357. executeListeners();
  2358. };
  2359. /**
  2360. * Returns the current coordinates.
  2361. * @public
  2362. * @return {object} Longitude/latitude couple
  2363. **/
  2364. this.getCoordinates = function() {
  2365. return {
  2366. longitude: long,
  2367. latitude: lat
  2368. };
  2369. };
  2370. /**
  2371. * Returns the current coordinates in degrees.
  2372. * @return {object} Longitude/latitude couple
  2373. **/
  2374. this.getCoordinatesInDegrees = function() {
  2375. return {
  2376. longitude: long_deg,
  2377. latitude: lat_deg
  2378. };
  2379. };
  2380. /**
  2381. * Returns the current screen orientation.
  2382. * @return {string|null} The screen orientation (portrait-primary, portrait-secondary, landscape-primary, landscape-secondary) or `null` if not supported
  2383. **/
  2384. this.getScreenOrientation = function() {
  2385. return orientation;
  2386. };
  2387. /**
  2388. * Adds a function to execute when device orientation changes.
  2389. * @public
  2390. * @param {function} f - The handler function
  2391. * @return {void}
  2392. **/
  2393. this.addListener = function(f) {
  2394. listeners.push(f);
  2395. };
  2396. /**
  2397. * Executes all the wanted functions for the main event.
  2398. * @private
  2399. * @return {void}
  2400. **/
  2401. var executeListeners = function() {
  2402. if (!!listeners.length) {
  2403. for (var i = 0, l = listeners.length; i < l; ++i) {
  2404. listeners[i]({
  2405. longitude: long,
  2406. latitude: lat
  2407. });
  2408. }
  2409. }
  2410. };
  2411. // Current state
  2412. var recording = false;
  2413. // Coordinates in degrees
  2414. var long_deg = 0, lat_deg = 0;
  2415. // Coordinates in radians
  2416. var long = 0, lat = 0;
  2417. // What a useful constant!
  2418. var DEG_TO_RAD = Math.PI / 180;
  2419. // Screen orientation
  2420. var orientation = Sphoords.getScreenOrientation();
  2421. // Browser engine
  2422. var engine = detectBrowserEngine();
  2423. // Listeners
  2424. var listeners = [];
  2425. };
  2426. /**
  2427. * Retrieves the current screen orientation.
  2428. * @static
  2429. * @return {string|null} Current screen orientation, `null` if not supported
  2430. **/
  2431. Sphoords.getScreenOrientation = function() {
  2432. var screen_orientation = null;
  2433. if (!!screen.orientation)
  2434. screen_orientation = screen.orientation;
  2435. else if (!!screen.mozOrientation)
  2436. screen_orientation = screen.mozOrientation;
  2437. else if (!!screen.msOrientation)
  2438. screen_orientation = screen.msOrientation;
  2439. else if (!!window.orientation || window.orientation === 0)
  2440. switch (window.orientation) {
  2441. case 0:
  2442. screen_orientation = 'portrait-primary';
  2443. break;
  2444. case 180:
  2445. screen_orientation = 'portrait-secondary';
  2446. break;
  2447. case -90:
  2448. screen_orientation = 'landscape-primary';
  2449. break;
  2450. case 90:
  2451. screen_orientation = 'landscape-secondary';
  2452. break;
  2453. }
  2454. // Are the specs respected?
  2455. return (screen_orientation !== null && (typeof screen_orientation == 'object')) ? screen_orientation.type : screen_orientation;
  2456. };
  2457. /**
  2458. * A boolean to know if device orientation is supported (`true` if it is, `false` otherwise).
  2459. * @static
  2460. **/
  2461. Sphoords.isDeviceOrientationSupported = false;
  2462. // Just testing if window.DeviceOrientationEvent is defined is not enough
  2463. // In fact, it can return false positives with some desktop browsers which run in computers that don't have the dedicated hardware
  2464. (function() {
  2465. // We attach the right event
  2466. // If it is fired, the API is really supported so we can indicate true :)
  2467. if (!!window.DeviceOrientationEvent && Sphoords.getScreenOrientation() !== null) {
  2468. function testDeviceOrientation(evt) {
  2469. if (evt !== null && evt.alpha !== null) {
  2470. Sphoords.isDeviceOrientationSupported = true;
  2471. window.removeEventListener('deviceorientation', testDeviceOrientation);
  2472. }
  2473. }
  2474. window.addEventListener('deviceorientation', testDeviceOrientation);
  2475. }
  2476. })();
  2477. export default PhotoSphereViewer