├── .gitignore ├── Gruntfile.js ├── LICENSE ├── README.md ├── build ├── demo.html ├── equirectangular-screenshot.jpg └── overpass-clip.mp4 ├── demo ├── build └── js │ ├── jquery-1.7.2.min.js │ └── three.min.js ├── package.json └── src ├── css ├── font-awesome.less ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ └── fontawesome-webfont.woff └── valiant360.less ├── lib ├── Detector.js ├── Three.js ├── modernizr-latest.js ├── require.js └── stats.min.js └── valiant.jquery.js /.gitignore: -------------------------------------------------------------------------------- 1 | videos/* 2 | node_modules/* 3 | css/main.css 4 | build/* -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*global module:false*/ 2 | module.exports = function(grunt) { 3 | 4 | // Project configuration. 5 | grunt.initConfig({ 6 | // Metadata. 7 | pkg: grunt.file.readJSON('package.json'), 8 | banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>\n' + 9 | ' <%= pkg.homepage ? "* " + pkg.homepage + "\\n" : "" %>' + 10 | ' * Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author %>;' + 11 | ' Licensed <%= pkg.license %> */\n\n', 12 | // Task configuration. 13 | concat: { 14 | options: { 15 | banner: '<%= banner %>' 16 | }, 17 | dist: { 18 | src: ['src/lib/Detector.js', 'src/valiant.jquery.js'], 19 | dest: 'build/<%= pkg.name %>.js' 20 | } 21 | }, 22 | copy: { 23 | build: { 24 | files: [ 25 | { expand: true, cwd: 'src/css/fonts/', src: ["*"], dest: "build/css/fonts/" }, 26 | { expand: true, cwd: 'demo/js', src: ["three.min.js", "jquery-1.7.2.min.js"], dest: "build/js" }, 27 | { src: ['README.md'], dest: "build/README.md" } 28 | ] 29 | }, 30 | to_site: { 31 | files: [ 32 | { expand: true, cwd: 'build/', src: ["**"], dest: "../flimshaw.github.io/Valiant360/build" }, 33 | { expand: true, cwd: 'dist/', src: ["<%= pkg.name %>_<%= pkg.version %>.zip"], dest: "../flimshaw.github.io/Valiant360/download/" } 34 | ] 35 | } 36 | }, 37 | bump: { 38 | options: { 39 | commit: false, 40 | push: false 41 | } 42 | }, 43 | uglify: { 44 | options: { 45 | banner: '<%= banner %>' 46 | }, 47 | dist: { 48 | src: '<%= concat.dist.dest %>', 49 | dest: 'build/<%= pkg.name %>.min.js' 50 | } 51 | }, 52 | jshint: { 53 | options: { 54 | curly: true, 55 | eqeqeq: true, 56 | immed: true, 57 | latedef: true, 58 | newcap: true, 59 | noarg: true, 60 | sub: true, 61 | undef: true, 62 | unused: true, 63 | boss: true, 64 | eqnull: true, 65 | laxcomma: true, 66 | multistr: true, 67 | browser: true, 68 | globals: { 69 | jQuery: true, 70 | console: true, 71 | THREE: true, 72 | Detector: true, 73 | requestAnimationFrame: true 74 | } 75 | }, 76 | lib: { 77 | src: 'src/valiantRefactor.js' 78 | }, 79 | gruntfile: { 80 | src: 'Gruntfile.js' 81 | }, 82 | lib_test: { 83 | src: ['lib/**/*.js', 'test/**/*.js'] 84 | } 85 | }, 86 | qunit: { 87 | files: ['test/**/*.html'] 88 | }, 89 | requirejs: { 90 | compile: { 91 | options: { 92 | baseUrl: "src/", 93 | mainConfigFile: "src/main.js", 94 | name: "main", // assumes a production build using almond 95 | out: "demo/valiant360.min.js" 96 | } 97 | } 98 | }, 99 | watch: { 100 | gruntfile: { 101 | files: '<%= jshint.gruntfile.src %>', 102 | tasks: ['jshint:gruntfile'] 103 | }, 104 | lib_test: { 105 | files: '<%= jshint.lib_test.src %>', 106 | tasks: ['jshint:lib_test', 'qunit'] 107 | }, 108 | main_css: { 109 | files: 'src/css/*.less', 110 | tasks: ['less'] 111 | }, 112 | dev_js: { 113 | files: ['src/valiant.jquery.js', 'src/valiantRefactor.js'], 114 | tasks: ['jshint:lib', 'concat'] 115 | } 116 | }, 117 | // gzip assets 1-to-1 for production 118 | compress: { 119 | main: { 120 | options: { 121 | archive: 'dist/<%= pkg.name %>_<%= pkg.version %>.zip' 122 | }, 123 | files: [ 124 | { expand: true, cwd: 'build/', src: ['**/*'], dest: './valiant360' } 125 | ] 126 | } 127 | }, 128 | rsync: { 129 | options: { 130 | args: ["--progress"], 131 | exclude: [".git*","*.scss","node_modules"], 132 | recursive: true 133 | }, 134 | demo: { 135 | options: { 136 | src: "build/demo/", 137 | dest: "../flimshaw.github.io/Valiant360/demo" 138 | } 139 | } 140 | }, 141 | less: { 142 | development: { 143 | options: { 144 | paths: ["src/css"], 145 | cleancss: true 146 | }, 147 | files: { 148 | "build/css/valiant360.css": "src/css/valiant360.less" 149 | } 150 | }, 151 | } 152 | }); 153 | 154 | // These plugins provide necessary tasks. 155 | grunt.loadNpmTasks('grunt-contrib-compress'); 156 | grunt.loadNpmTasks('grunt-contrib-concat'); 157 | grunt.loadNpmTasks('grunt-contrib-uglify'); 158 | grunt.loadNpmTasks('grunt-contrib-qunit'); 159 | grunt.loadNpmTasks('grunt-contrib-jshint'); 160 | grunt.loadNpmTasks('grunt-contrib-watch'); 161 | grunt.loadNpmTasks('grunt-contrib-less'); 162 | grunt.loadNpmTasks('grunt-contrib-copy'); 163 | grunt.loadNpmTasks('grunt-contrib-requirejs'); 164 | grunt.loadNpmTasks('grunt-rsync'); 165 | grunt.loadNpmTasks('grunt-bump'); 166 | 167 | // Default task. 168 | grunt.registerTask('default', ['jshint', 'less']); 169 | grunt.registerTask('build', ['default', 'copy:build', 'concat', 'uglify']); 170 | grunt.registerTask('deploy', ['default', 'copy:build', 'concat', 'uglify', 'compress', 'copy:to_site']); 171 | 172 | }; 173 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2014 Charlie Hoey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Valiant 360 (beta) 2 | 3 | ### A browser-based video player for 360 degree panorama videos and photos. 4 | 5 | [Example](http://flimshaw.github.io/Valiant360) - [Development Log](https://github.com/flimshaw/Valiant360/wiki/Development-log) 6 | 7 | 8 | #### About 9 | 10 | The aim of this project is to provide a free, minimalist 360 degree video WebGL player for modern browsers. It is implemented as a jQuery plugin, with a limited interface for controlling video playback, and mouse/scrollwheel controls for zooming and panning. 11 | 12 | There is currently no mobile support, but as Chrome and Safari mobile editions enable WebGL, this should be forwards-compatible with them. 13 | 14 | #### Usage 15 | 16 | See the [demo folder](https://github.com/flimshaw/Valiant360/tree/master/demo) or the [example](http://flimshaw.github.io/Valiant360). Moving the mouse will pan the camera, and the scroll wheel will zoom in and out. 17 | 18 | **Markup** 19 | 20 | On the HTML side, create a div to act as your container, and add a data-video-src attribute pointing to the video file you wish to play. 21 | 22 | ``` 23 |
24 | ``` 25 | 26 | Or, if you wish to use it to view a photo (note: currently must be powers-of-2 resolution (ie. 2048x1024): 27 | ``` 28 |
29 | ``` 30 | 31 | **Javascript** 32 | 33 | More detailed api documentation pending, for now the below explains about all you can do. 34 | 35 | ``` 36 | // initialize plugin, default options shown 37 | $('.valiantContainer').Valiant360({ 38 | crossOrigin: 'anonymous', // valid keywords: 'anonymous' or 'use-credentials' 39 | clickAndDrag: false, // use click-and-drag camera controls 40 | keyboardControls: true, // use keyboard controls (move by arrows) 41 | flatProjection: false, // map image to appear flat (often more distorted) 42 | fov: 35, // initial field of view 43 | fovMin: 3, // min field of view allowed 44 | fovMax: 100, // max field of view allowed 45 | hideControls: false, // hide player controls 46 | lon: 0, // initial lon for camera angle 47 | lat: 0, // initial lat for camera angle 48 | loop: "loop", // video loops by default 49 | muted: true, // video muted by default 50 | volume: 0.5, // video volume by default 51 | autoplay: true // video autoplays by default 52 | }); 53 | 54 | // play video 55 | $('.valiantContainer').Valiant360('play'); 56 | 57 | // pause video 58 | $('.valiantContainer').Valiant360('pause'); 59 | 60 | // load new video file 61 | $('.valiantContainer').Valiant360('loadVideo', 'path/to/file.mp4'); 62 | 63 | // load new photo file 64 | $('.valiantContainer').Valiant360('loadPhoto', 'path/to/file.jpg'); 65 | 66 | // destroy Valiant360 processing/resources (however, will not remove element from the dom. That is left up to you) 67 | $('.valiantContainer').Valiant360('destroy'); 68 | 69 | ``` 70 | 71 | #### A note on the crossOrigin CORS option 72 | Allows images and videos to be served from a domain separate to where Valiant360 is hosted (eg a CDN). **If a crossOrigin keyword is not specified, anonymous is used**. 73 | 74 | This option will allow Valiant360 to grab cross-domain assets for Chrome and Firefox, however at time of writing Safari throws the error: `[Error] SecurityError: DOM Exception 18: An attempt was made to break through the security policy of the user agent.` 75 | 76 | Cross-domain tested on Mac OSX Yosemite: Chrome v43.0.2357.130, Chrome Canary v45.0.2449.0, Firefox v39.0, Safari v8.0.6. 77 | 78 | For further explanation on these CORS keywords, see: 79 | * [MDN CORS settings attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes) 80 | * [WHATWG CORS settings attributes](https://html.spec.whatwg.org/multipage/infrastructure.html#cors-settings-attribute) 81 | 82 | #### 3rd party libraries and their licenses 83 | 84 | The following assets are used in this tool's creation. 85 | 86 | + [JQuery 1.7.2+](http://jquery.com) (MIT License) 87 | + [Three.js](http://threejs.org/) + Detector (MIT License) 88 | + [Font Awesome](http://fortawesome.github.io/Font-Awesome/) (MIT License) -------------------------------------------------------------------------------- /build/demo.html: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | Valiant 360 Pano Player 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 39 | 40 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /build/equirectangular-screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flimshaw/Valiant360/79f1b528886ab580e1624ef3b5857dbd541d62c4/build/equirectangular-screenshot.jpg -------------------------------------------------------------------------------- /build/overpass-clip.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flimshaw/Valiant360/79f1b528886ab580e1624ef3b5857dbd541d62c4/build/overpass-clip.mp4 -------------------------------------------------------------------------------- /demo/build: -------------------------------------------------------------------------------- 1 | ../build -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery.valiant360", 3 | "description": "jQuery plugin for playing 360 degree videos using WebGL", 4 | "homepage": "http://flimshaw.github.io/Valiant360", 5 | "version": "0.4.3", 6 | "author": "Charlie Hoey ", 7 | "license": "MIT", 8 | "engines": { 9 | "node": ">= 0.10.0" 10 | }, 11 | "devDependencies": { 12 | "grunt": "~0.4.2", 13 | "grunt-contrib-jshint": "~0.7.2", 14 | "grunt-contrib-watch": "~0.5.3", 15 | "grunt-contrib-qunit": "~0.3.0", 16 | "grunt-contrib-concat": "~0.3.0", 17 | "grunt-contrib-uglify": "~0.2.7", 18 | "grunt-contrib-less": "~0.8.3", 19 | "grunt-contrib-copy": "~0.5.0", 20 | "grunt-contrib-requirejs": "~0.4.1", 21 | "grunt-rsync": "~0.2.1", 22 | "grunt-bump": "0.0.13", 23 | "grunt-contrib-compress": "~0.6.0", 24 | "grunt-lib-contrib": "~0.6.1", 25 | "less": "~1.5.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/css/font-awesome.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.0.3 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | /* FONT PATH 6 | * -------------------------- */ 7 | @font-face { 8 | font-family: 'FontAwesome'; 9 | src: url('fonts/fontawesome-webfont.eot?v=4.0.3'); 10 | src: url('fonts/fontawesome-webfont.eot?#iefix&v=4.0.3') format('embedded-opentype'), url('fonts/fontawesome-webfont.woff?v=4.0.3') format('woff'), url('fonts/fontawesome-webfont.ttf?v=4.0.3') format('truetype'), url('fonts/fontawesome-webfont.svg?v=4.0.3#fontawesomeregular') format('svg'); 11 | font-weight: normal; 12 | font-style: normal; 13 | } 14 | .fa { 15 | display: inline-block; 16 | font-family: FontAwesome; 17 | font-style: normal; 18 | font-weight: normal; 19 | line-height: 1; 20 | -webkit-font-smoothing: antialiased; 21 | -moz-osx-font-smoothing: grayscale; 22 | } 23 | /* makes the font 33% larger relative to the icon container */ 24 | .fa-lg { 25 | font-size: 1.3333333333333333em; 26 | line-height: 0.75em; 27 | vertical-align: -15%; 28 | } 29 | .fa-2x { 30 | font-size: 2em; 31 | } 32 | .fa-3x { 33 | font-size: 3em; 34 | } 35 | .fa-4x { 36 | font-size: 4em; 37 | } 38 | .fa-5x { 39 | font-size: 5em; 40 | } 41 | .fa-fw { 42 | width: 1.2857142857142858em; 43 | text-align: center; 44 | } 45 | .fa-ul { 46 | padding-left: 0; 47 | margin-left: 2.142857142857143em; 48 | list-style-type: none; 49 | } 50 | .fa-ul > li { 51 | position: relative; 52 | } 53 | .fa-li { 54 | position: absolute; 55 | left: -2.142857142857143em; 56 | width: 2.142857142857143em; 57 | top: 0.14285714285714285em; 58 | text-align: center; 59 | } 60 | .fa-li.fa-lg { 61 | left: -1.8571428571428572em; 62 | } 63 | .fa-border { 64 | padding: .2em .25em .15em; 65 | border: solid 0.08em #eeeeee; 66 | border-radius: .1em; 67 | } 68 | .pull-right { 69 | float: right; 70 | } 71 | .pull-left { 72 | float: left; 73 | } 74 | .fa.pull-left { 75 | margin-right: .3em; 76 | } 77 | .fa.pull-right { 78 | margin-left: .3em; 79 | } 80 | .fa-spin { 81 | -webkit-animation: spin 2s infinite linear; 82 | -moz-animation: spin 2s infinite linear; 83 | -o-animation: spin 2s infinite linear; 84 | animation: spin 2s infinite linear; 85 | } 86 | @-moz-keyframes spin { 87 | 0% { 88 | -moz-transform: rotate(0deg); 89 | } 90 | 100% { 91 | -moz-transform: rotate(359deg); 92 | } 93 | } 94 | @-webkit-keyframes spin { 95 | 0% { 96 | -webkit-transform: rotate(0deg); 97 | } 98 | 100% { 99 | -webkit-transform: rotate(359deg); 100 | } 101 | } 102 | @-o-keyframes spin { 103 | 0% { 104 | -o-transform: rotate(0deg); 105 | } 106 | 100% { 107 | -o-transform: rotate(359deg); 108 | } 109 | } 110 | @-ms-keyframes spin { 111 | 0% { 112 | -ms-transform: rotate(0deg); 113 | } 114 | 100% { 115 | -ms-transform: rotate(359deg); 116 | } 117 | } 118 | @keyframes spin { 119 | 0% { 120 | transform: rotate(0deg); 121 | } 122 | 100% { 123 | transform: rotate(359deg); 124 | } 125 | } 126 | .fa-rotate-90 { 127 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1); 128 | -webkit-transform: rotate(90deg); 129 | -moz-transform: rotate(90deg); 130 | -ms-transform: rotate(90deg); 131 | -o-transform: rotate(90deg); 132 | transform: rotate(90deg); 133 | } 134 | .fa-rotate-180 { 135 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2); 136 | -webkit-transform: rotate(180deg); 137 | -moz-transform: rotate(180deg); 138 | -ms-transform: rotate(180deg); 139 | -o-transform: rotate(180deg); 140 | transform: rotate(180deg); 141 | } 142 | .fa-rotate-270 { 143 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); 144 | -webkit-transform: rotate(270deg); 145 | -moz-transform: rotate(270deg); 146 | -ms-transform: rotate(270deg); 147 | -o-transform: rotate(270deg); 148 | transform: rotate(270deg); 149 | } 150 | .fa-flip-horizontal { 151 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1); 152 | -webkit-transform: scale(-1, 1); 153 | -moz-transform: scale(-1, 1); 154 | -ms-transform: scale(-1, 1); 155 | -o-transform: scale(-1, 1); 156 | transform: scale(-1, 1); 157 | } 158 | .fa-flip-vertical { 159 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1); 160 | -webkit-transform: scale(1, -1); 161 | -moz-transform: scale(1, -1); 162 | -ms-transform: scale(1, -1); 163 | -o-transform: scale(1, -1); 164 | transform: scale(1, -1); 165 | } 166 | .fa-stack { 167 | position: relative; 168 | display: inline-block; 169 | width: 2em; 170 | height: 2em; 171 | line-height: 2em; 172 | vertical-align: middle; 173 | } 174 | .fa-stack-1x, 175 | .fa-stack-2x { 176 | position: absolute; 177 | left: 0; 178 | width: 100%; 179 | text-align: center; 180 | } 181 | .fa-stack-1x { 182 | line-height: inherit; 183 | } 184 | .fa-stack-2x { 185 | font-size: 2em; 186 | } 187 | .fa-inverse { 188 | color: #ffffff; 189 | } 190 | /* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen 191 | readers do not read off random characters that represent icons */ 192 | .fa-glass:before { 193 | content: "\f000"; 194 | } 195 | .fa-music:before { 196 | content: "\f001"; 197 | } 198 | .fa-search:before { 199 | content: "\f002"; 200 | } 201 | .fa-envelope-o:before { 202 | content: "\f003"; 203 | } 204 | .fa-heart:before { 205 | content: "\f004"; 206 | } 207 | .fa-star:before { 208 | content: "\f005"; 209 | } 210 | .fa-star-o:before { 211 | content: "\f006"; 212 | } 213 | .fa-user:before { 214 | content: "\f007"; 215 | } 216 | .fa-film:before { 217 | content: "\f008"; 218 | } 219 | .fa-th-large:before { 220 | content: "\f009"; 221 | } 222 | .fa-th:before { 223 | content: "\f00a"; 224 | } 225 | .fa-th-list:before { 226 | content: "\f00b"; 227 | } 228 | .fa-check:before { 229 | content: "\f00c"; 230 | } 231 | .fa-times:before { 232 | content: "\f00d"; 233 | } 234 | .fa-search-plus:before { 235 | content: "\f00e"; 236 | } 237 | .fa-search-minus:before { 238 | content: "\f010"; 239 | } 240 | .fa-power-off:before { 241 | content: "\f011"; 242 | } 243 | .fa-signal:before { 244 | content: "\f012"; 245 | } 246 | .fa-gear:before, 247 | .fa-cog:before { 248 | content: "\f013"; 249 | } 250 | .fa-trash-o:before { 251 | content: "\f014"; 252 | } 253 | .fa-home:before { 254 | content: "\f015"; 255 | } 256 | .fa-file-o:before { 257 | content: "\f016"; 258 | } 259 | .fa-clock-o:before { 260 | content: "\f017"; 261 | } 262 | .fa-road:before { 263 | content: "\f018"; 264 | } 265 | .fa-download:before { 266 | content: "\f019"; 267 | } 268 | .fa-arrow-circle-o-down:before { 269 | content: "\f01a"; 270 | } 271 | .fa-arrow-circle-o-up:before { 272 | content: "\f01b"; 273 | } 274 | .fa-inbox:before { 275 | content: "\f01c"; 276 | } 277 | .fa-play-circle-o:before { 278 | content: "\f01d"; 279 | } 280 | .fa-rotate-right:before, 281 | .fa-repeat:before { 282 | content: "\f01e"; 283 | } 284 | .fa-refresh:before { 285 | content: "\f021"; 286 | } 287 | .fa-list-alt:before { 288 | content: "\f022"; 289 | } 290 | .fa-lock:before { 291 | content: "\f023"; 292 | } 293 | .fa-flag:before { 294 | content: "\f024"; 295 | } 296 | .fa-headphones:before { 297 | content: "\f025"; 298 | } 299 | .fa-volume-off:before { 300 | content: "\f026"; 301 | } 302 | .fa-volume-down:before { 303 | content: "\f027"; 304 | } 305 | .fa-volume-up:before { 306 | content: "\f028"; 307 | } 308 | .fa-qrcode:before { 309 | content: "\f029"; 310 | } 311 | .fa-barcode:before { 312 | content: "\f02a"; 313 | } 314 | .fa-tag:before { 315 | content: "\f02b"; 316 | } 317 | .fa-tags:before { 318 | content: "\f02c"; 319 | } 320 | .fa-book:before { 321 | content: "\f02d"; 322 | } 323 | .fa-bookmark:before { 324 | content: "\f02e"; 325 | } 326 | .fa-print:before { 327 | content: "\f02f"; 328 | } 329 | .fa-camera:before { 330 | content: "\f030"; 331 | } 332 | .fa-font:before { 333 | content: "\f031"; 334 | } 335 | .fa-bold:before { 336 | content: "\f032"; 337 | } 338 | .fa-italic:before { 339 | content: "\f033"; 340 | } 341 | .fa-text-height:before { 342 | content: "\f034"; 343 | } 344 | .fa-text-width:before { 345 | content: "\f035"; 346 | } 347 | .fa-align-left:before { 348 | content: "\f036"; 349 | } 350 | .fa-align-center:before { 351 | content: "\f037"; 352 | } 353 | .fa-align-right:before { 354 | content: "\f038"; 355 | } 356 | .fa-align-justify:before { 357 | content: "\f039"; 358 | } 359 | .fa-list:before { 360 | content: "\f03a"; 361 | } 362 | .fa-dedent:before, 363 | .fa-outdent:before { 364 | content: "\f03b"; 365 | } 366 | .fa-indent:before { 367 | content: "\f03c"; 368 | } 369 | .fa-video-camera:before { 370 | content: "\f03d"; 371 | } 372 | .fa-picture-o:before { 373 | content: "\f03e"; 374 | } 375 | .fa-pencil:before { 376 | content: "\f040"; 377 | } 378 | .fa-map-marker:before { 379 | content: "\f041"; 380 | } 381 | .fa-adjust:before { 382 | content: "\f042"; 383 | } 384 | .fa-tint:before { 385 | content: "\f043"; 386 | } 387 | .fa-edit:before, 388 | .fa-pencil-square-o:before { 389 | content: "\f044"; 390 | } 391 | .fa-share-square-o:before { 392 | content: "\f045"; 393 | } 394 | .fa-check-square-o:before { 395 | content: "\f046"; 396 | } 397 | .fa-arrows:before { 398 | content: "\f047"; 399 | } 400 | .fa-step-backward:before { 401 | content: "\f048"; 402 | } 403 | .fa-fast-backward:before { 404 | content: "\f049"; 405 | } 406 | .fa-backward:before { 407 | content: "\f04a"; 408 | } 409 | .fa-play:before { 410 | content: "\f04b"; 411 | } 412 | .fa-pause:before { 413 | content: "\f04c"; 414 | } 415 | .fa-stop:before { 416 | content: "\f04d"; 417 | } 418 | .fa-forward:before { 419 | content: "\f04e"; 420 | } 421 | .fa-fast-forward:before { 422 | content: "\f050"; 423 | } 424 | .fa-step-forward:before { 425 | content: "\f051"; 426 | } 427 | .fa-eject:before { 428 | content: "\f052"; 429 | } 430 | .fa-chevron-left:before { 431 | content: "\f053"; 432 | } 433 | .fa-chevron-right:before { 434 | content: "\f054"; 435 | } 436 | .fa-plus-circle:before { 437 | content: "\f055"; 438 | } 439 | .fa-minus-circle:before { 440 | content: "\f056"; 441 | } 442 | .fa-times-circle:before { 443 | content: "\f057"; 444 | } 445 | .fa-check-circle:before { 446 | content: "\f058"; 447 | } 448 | .fa-question-circle:before { 449 | content: "\f059"; 450 | } 451 | .fa-info-circle:before { 452 | content: "\f05a"; 453 | } 454 | .fa-crosshairs:before { 455 | content: "\f05b"; 456 | } 457 | .fa-times-circle-o:before { 458 | content: "\f05c"; 459 | } 460 | .fa-check-circle-o:before { 461 | content: "\f05d"; 462 | } 463 | .fa-ban:before { 464 | content: "\f05e"; 465 | } 466 | .fa-arrow-left:before { 467 | content: "\f060"; 468 | } 469 | .fa-arrow-right:before { 470 | content: "\f061"; 471 | } 472 | .fa-arrow-up:before { 473 | content: "\f062"; 474 | } 475 | .fa-arrow-down:before { 476 | content: "\f063"; 477 | } 478 | .fa-mail-forward:before, 479 | .fa-share:before { 480 | content: "\f064"; 481 | } 482 | .fa-expand:before { 483 | content: "\f065"; 484 | } 485 | .fa-compress:before { 486 | content: "\f066"; 487 | } 488 | .fa-plus:before { 489 | content: "\f067"; 490 | } 491 | .fa-minus:before { 492 | content: "\f068"; 493 | } 494 | .fa-asterisk:before { 495 | content: "\f069"; 496 | } 497 | .fa-exclamation-circle:before { 498 | content: "\f06a"; 499 | } 500 | .fa-gift:before { 501 | content: "\f06b"; 502 | } 503 | .fa-leaf:before { 504 | content: "\f06c"; 505 | } 506 | .fa-fire:before { 507 | content: "\f06d"; 508 | } 509 | .fa-eye:before { 510 | content: "\f06e"; 511 | } 512 | .fa-eye-slash:before { 513 | content: "\f070"; 514 | } 515 | .fa-warning:before, 516 | .fa-exclamation-triangle:before { 517 | content: "\f071"; 518 | } 519 | .fa-plane:before { 520 | content: "\f072"; 521 | } 522 | .fa-calendar:before { 523 | content: "\f073"; 524 | } 525 | .fa-random:before { 526 | content: "\f074"; 527 | } 528 | .fa-comment:before { 529 | content: "\f075"; 530 | } 531 | .fa-magnet:before { 532 | content: "\f076"; 533 | } 534 | .fa-chevron-up:before { 535 | content: "\f077"; 536 | } 537 | .fa-chevron-down:before { 538 | content: "\f078"; 539 | } 540 | .fa-retweet:before { 541 | content: "\f079"; 542 | } 543 | .fa-shopping-cart:before { 544 | content: "\f07a"; 545 | } 546 | .fa-folder:before { 547 | content: "\f07b"; 548 | } 549 | .fa-folder-open:before { 550 | content: "\f07c"; 551 | } 552 | .fa-arrows-v:before { 553 | content: "\f07d"; 554 | } 555 | .fa-arrows-h:before { 556 | content: "\f07e"; 557 | } 558 | .fa-bar-chart-o:before { 559 | content: "\f080"; 560 | } 561 | .fa-twitter-square:before { 562 | content: "\f081"; 563 | } 564 | .fa-facebook-square:before { 565 | content: "\f082"; 566 | } 567 | .fa-camera-retro:before { 568 | content: "\f083"; 569 | } 570 | .fa-key:before { 571 | content: "\f084"; 572 | } 573 | .fa-gears:before, 574 | .fa-cogs:before { 575 | content: "\f085"; 576 | } 577 | .fa-comments:before { 578 | content: "\f086"; 579 | } 580 | .fa-thumbs-o-up:before { 581 | content: "\f087"; 582 | } 583 | .fa-thumbs-o-down:before { 584 | content: "\f088"; 585 | } 586 | .fa-star-half:before { 587 | content: "\f089"; 588 | } 589 | .fa-heart-o:before { 590 | content: "\f08a"; 591 | } 592 | .fa-sign-out:before { 593 | content: "\f08b"; 594 | } 595 | .fa-linkedin-square:before { 596 | content: "\f08c"; 597 | } 598 | .fa-thumb-tack:before { 599 | content: "\f08d"; 600 | } 601 | .fa-external-link:before { 602 | content: "\f08e"; 603 | } 604 | .fa-sign-in:before { 605 | content: "\f090"; 606 | } 607 | .fa-trophy:before { 608 | content: "\f091"; 609 | } 610 | .fa-github-square:before { 611 | content: "\f092"; 612 | } 613 | .fa-upload:before { 614 | content: "\f093"; 615 | } 616 | .fa-lemon-o:before { 617 | content: "\f094"; 618 | } 619 | .fa-phone:before { 620 | content: "\f095"; 621 | } 622 | .fa-square-o:before { 623 | content: "\f096"; 624 | } 625 | .fa-bookmark-o:before { 626 | content: "\f097"; 627 | } 628 | .fa-phone-square:before { 629 | content: "\f098"; 630 | } 631 | .fa-twitter:before { 632 | content: "\f099"; 633 | } 634 | .fa-facebook:before { 635 | content: "\f09a"; 636 | } 637 | .fa-github:before { 638 | content: "\f09b"; 639 | } 640 | .fa-unlock:before { 641 | content: "\f09c"; 642 | } 643 | .fa-credit-card:before { 644 | content: "\f09d"; 645 | } 646 | .fa-rss:before { 647 | content: "\f09e"; 648 | } 649 | .fa-hdd-o:before { 650 | content: "\f0a0"; 651 | } 652 | .fa-bullhorn:before { 653 | content: "\f0a1"; 654 | } 655 | .fa-bell:before { 656 | content: "\f0f3"; 657 | } 658 | .fa-certificate:before { 659 | content: "\f0a3"; 660 | } 661 | .fa-hand-o-right:before { 662 | content: "\f0a4"; 663 | } 664 | .fa-hand-o-left:before { 665 | content: "\f0a5"; 666 | } 667 | .fa-hand-o-up:before { 668 | content: "\f0a6"; 669 | } 670 | .fa-hand-o-down:before { 671 | content: "\f0a7"; 672 | } 673 | .fa-arrow-circle-left:before { 674 | content: "\f0a8"; 675 | } 676 | .fa-arrow-circle-right:before { 677 | content: "\f0a9"; 678 | } 679 | .fa-arrow-circle-up:before { 680 | content: "\f0aa"; 681 | } 682 | .fa-arrow-circle-down:before { 683 | content: "\f0ab"; 684 | } 685 | .fa-globe:before { 686 | content: "\f0ac"; 687 | } 688 | .fa-wrench:before { 689 | content: "\f0ad"; 690 | } 691 | .fa-tasks:before { 692 | content: "\f0ae"; 693 | } 694 | .fa-filter:before { 695 | content: "\f0b0"; 696 | } 697 | .fa-briefcase:before { 698 | content: "\f0b1"; 699 | } 700 | .fa-arrows-alt:before { 701 | content: "\f0b2"; 702 | } 703 | .fa-group:before, 704 | .fa-users:before { 705 | content: "\f0c0"; 706 | } 707 | .fa-chain:before, 708 | .fa-link:before { 709 | content: "\f0c1"; 710 | } 711 | .fa-cloud:before { 712 | content: "\f0c2"; 713 | } 714 | .fa-flask:before { 715 | content: "\f0c3"; 716 | } 717 | .fa-cut:before, 718 | .fa-scissors:before { 719 | content: "\f0c4"; 720 | } 721 | .fa-copy:before, 722 | .fa-files-o:before { 723 | content: "\f0c5"; 724 | } 725 | .fa-paperclip:before { 726 | content: "\f0c6"; 727 | } 728 | .fa-save:before, 729 | .fa-floppy-o:before { 730 | content: "\f0c7"; 731 | } 732 | .fa-square:before { 733 | content: "\f0c8"; 734 | } 735 | .fa-bars:before { 736 | content: "\f0c9"; 737 | } 738 | .fa-list-ul:before { 739 | content: "\f0ca"; 740 | } 741 | .fa-list-ol:before { 742 | content: "\f0cb"; 743 | } 744 | .fa-strikethrough:before { 745 | content: "\f0cc"; 746 | } 747 | .fa-underline:before { 748 | content: "\f0cd"; 749 | } 750 | .fa-table:before { 751 | content: "\f0ce"; 752 | } 753 | .fa-magic:before { 754 | content: "\f0d0"; 755 | } 756 | .fa-truck:before { 757 | content: "\f0d1"; 758 | } 759 | .fa-pinterest:before { 760 | content: "\f0d2"; 761 | } 762 | .fa-pinterest-square:before { 763 | content: "\f0d3"; 764 | } 765 | .fa-google-plus-square:before { 766 | content: "\f0d4"; 767 | } 768 | .fa-google-plus:before { 769 | content: "\f0d5"; 770 | } 771 | .fa-money:before { 772 | content: "\f0d6"; 773 | } 774 | .fa-caret-down:before { 775 | content: "\f0d7"; 776 | } 777 | .fa-caret-up:before { 778 | content: "\f0d8"; 779 | } 780 | .fa-caret-left:before { 781 | content: "\f0d9"; 782 | } 783 | .fa-caret-right:before { 784 | content: "\f0da"; 785 | } 786 | .fa-columns:before { 787 | content: "\f0db"; 788 | } 789 | .fa-unsorted:before, 790 | .fa-sort:before { 791 | content: "\f0dc"; 792 | } 793 | .fa-sort-down:before, 794 | .fa-sort-asc:before { 795 | content: "\f0dd"; 796 | } 797 | .fa-sort-up:before, 798 | .fa-sort-desc:before { 799 | content: "\f0de"; 800 | } 801 | .fa-envelope:before { 802 | content: "\f0e0"; 803 | } 804 | .fa-linkedin:before { 805 | content: "\f0e1"; 806 | } 807 | .fa-rotate-left:before, 808 | .fa-undo:before { 809 | content: "\f0e2"; 810 | } 811 | .fa-legal:before, 812 | .fa-gavel:before { 813 | content: "\f0e3"; 814 | } 815 | .fa-dashboard:before, 816 | .fa-tachometer:before { 817 | content: "\f0e4"; 818 | } 819 | .fa-comment-o:before { 820 | content: "\f0e5"; 821 | } 822 | .fa-comments-o:before { 823 | content: "\f0e6"; 824 | } 825 | .fa-flash:before, 826 | .fa-bolt:before { 827 | content: "\f0e7"; 828 | } 829 | .fa-sitemap:before { 830 | content: "\f0e8"; 831 | } 832 | .fa-umbrella:before { 833 | content: "\f0e9"; 834 | } 835 | .fa-paste:before, 836 | .fa-clipboard:before { 837 | content: "\f0ea"; 838 | } 839 | .fa-lightbulb-o:before { 840 | content: "\f0eb"; 841 | } 842 | .fa-exchange:before { 843 | content: "\f0ec"; 844 | } 845 | .fa-cloud-download:before { 846 | content: "\f0ed"; 847 | } 848 | .fa-cloud-upload:before { 849 | content: "\f0ee"; 850 | } 851 | .fa-user-md:before { 852 | content: "\f0f0"; 853 | } 854 | .fa-stethoscope:before { 855 | content: "\f0f1"; 856 | } 857 | .fa-suitcase:before { 858 | content: "\f0f2"; 859 | } 860 | .fa-bell-o:before { 861 | content: "\f0a2"; 862 | } 863 | .fa-coffee:before { 864 | content: "\f0f4"; 865 | } 866 | .fa-cutlery:before { 867 | content: "\f0f5"; 868 | } 869 | .fa-file-text-o:before { 870 | content: "\f0f6"; 871 | } 872 | .fa-building-o:before { 873 | content: "\f0f7"; 874 | } 875 | .fa-hospital-o:before { 876 | content: "\f0f8"; 877 | } 878 | .fa-ambulance:before { 879 | content: "\f0f9"; 880 | } 881 | .fa-medkit:before { 882 | content: "\f0fa"; 883 | } 884 | .fa-fighter-jet:before { 885 | content: "\f0fb"; 886 | } 887 | .fa-beer:before { 888 | content: "\f0fc"; 889 | } 890 | .fa-h-square:before { 891 | content: "\f0fd"; 892 | } 893 | .fa-plus-square:before { 894 | content: "\f0fe"; 895 | } 896 | .fa-angle-double-left:before { 897 | content: "\f100"; 898 | } 899 | .fa-angle-double-right:before { 900 | content: "\f101"; 901 | } 902 | .fa-angle-double-up:before { 903 | content: "\f102"; 904 | } 905 | .fa-angle-double-down:before { 906 | content: "\f103"; 907 | } 908 | .fa-angle-left:before { 909 | content: "\f104"; 910 | } 911 | .fa-angle-right:before { 912 | content: "\f105"; 913 | } 914 | .fa-angle-up:before { 915 | content: "\f106"; 916 | } 917 | .fa-angle-down:before { 918 | content: "\f107"; 919 | } 920 | .fa-desktop:before { 921 | content: "\f108"; 922 | } 923 | .fa-laptop:before { 924 | content: "\f109"; 925 | } 926 | .fa-tablet:before { 927 | content: "\f10a"; 928 | } 929 | .fa-mobile-phone:before, 930 | .fa-mobile:before { 931 | content: "\f10b"; 932 | } 933 | .fa-circle-o:before { 934 | content: "\f10c"; 935 | } 936 | .fa-quote-left:before { 937 | content: "\f10d"; 938 | } 939 | .fa-quote-right:before { 940 | content: "\f10e"; 941 | } 942 | .fa-spinner:before { 943 | content: "\f110"; 944 | } 945 | .fa-circle:before { 946 | content: "\f111"; 947 | } 948 | .fa-mail-reply:before, 949 | .fa-reply:before { 950 | content: "\f112"; 951 | } 952 | .fa-github-alt:before { 953 | content: "\f113"; 954 | } 955 | .fa-folder-o:before { 956 | content: "\f114"; 957 | } 958 | .fa-folder-open-o:before { 959 | content: "\f115"; 960 | } 961 | .fa-smile-o:before { 962 | content: "\f118"; 963 | } 964 | .fa-frown-o:before { 965 | content: "\f119"; 966 | } 967 | .fa-meh-o:before { 968 | content: "\f11a"; 969 | } 970 | .fa-gamepad:before { 971 | content: "\f11b"; 972 | } 973 | .fa-keyboard-o:before { 974 | content: "\f11c"; 975 | } 976 | .fa-flag-o:before { 977 | content: "\f11d"; 978 | } 979 | .fa-flag-checkered:before { 980 | content: "\f11e"; 981 | } 982 | .fa-terminal:before { 983 | content: "\f120"; 984 | } 985 | .fa-code:before { 986 | content: "\f121"; 987 | } 988 | .fa-reply-all:before { 989 | content: "\f122"; 990 | } 991 | .fa-mail-reply-all:before { 992 | content: "\f122"; 993 | } 994 | .fa-star-half-empty:before, 995 | .fa-star-half-full:before, 996 | .fa-star-half-o:before { 997 | content: "\f123"; 998 | } 999 | .fa-location-arrow:before { 1000 | content: "\f124"; 1001 | } 1002 | .fa-crop:before { 1003 | content: "\f125"; 1004 | } 1005 | .fa-code-fork:before { 1006 | content: "\f126"; 1007 | } 1008 | .fa-unlink:before, 1009 | .fa-chain-broken:before { 1010 | content: "\f127"; 1011 | } 1012 | .fa-question:before { 1013 | content: "\f128"; 1014 | } 1015 | .fa-info:before { 1016 | content: "\f129"; 1017 | } 1018 | .fa-exclamation:before { 1019 | content: "\f12a"; 1020 | } 1021 | .fa-superscript:before { 1022 | content: "\f12b"; 1023 | } 1024 | .fa-subscript:before { 1025 | content: "\f12c"; 1026 | } 1027 | .fa-eraser:before { 1028 | content: "\f12d"; 1029 | } 1030 | .fa-puzzle-piece:before { 1031 | content: "\f12e"; 1032 | } 1033 | .fa-microphone:before { 1034 | content: "\f130"; 1035 | } 1036 | .fa-microphone-slash:before { 1037 | content: "\f131"; 1038 | } 1039 | .fa-shield:before { 1040 | content: "\f132"; 1041 | } 1042 | .fa-calendar-o:before { 1043 | content: "\f133"; 1044 | } 1045 | .fa-fire-extinguisher:before { 1046 | content: "\f134"; 1047 | } 1048 | .fa-rocket:before { 1049 | content: "\f135"; 1050 | } 1051 | .fa-maxcdn:before { 1052 | content: "\f136"; 1053 | } 1054 | .fa-chevron-circle-left:before { 1055 | content: "\f137"; 1056 | } 1057 | .fa-chevron-circle-right:before { 1058 | content: "\f138"; 1059 | } 1060 | .fa-chevron-circle-up:before { 1061 | content: "\f139"; 1062 | } 1063 | .fa-chevron-circle-down:before { 1064 | content: "\f13a"; 1065 | } 1066 | .fa-html5:before { 1067 | content: "\f13b"; 1068 | } 1069 | .fa-css3:before { 1070 | content: "\f13c"; 1071 | } 1072 | .fa-anchor:before { 1073 | content: "\f13d"; 1074 | } 1075 | .fa-unlock-alt:before { 1076 | content: "\f13e"; 1077 | } 1078 | .fa-bullseye:before { 1079 | content: "\f140"; 1080 | } 1081 | .fa-ellipsis-h:before { 1082 | content: "\f141"; 1083 | } 1084 | .fa-ellipsis-v:before { 1085 | content: "\f142"; 1086 | } 1087 | .fa-rss-square:before { 1088 | content: "\f143"; 1089 | } 1090 | .fa-play-circle:before { 1091 | content: "\f144"; 1092 | } 1093 | .fa-ticket:before { 1094 | content: "\f145"; 1095 | } 1096 | .fa-minus-square:before { 1097 | content: "\f146"; 1098 | } 1099 | .fa-minus-square-o:before { 1100 | content: "\f147"; 1101 | } 1102 | .fa-level-up:before { 1103 | content: "\f148"; 1104 | } 1105 | .fa-level-down:before { 1106 | content: "\f149"; 1107 | } 1108 | .fa-check-square:before { 1109 | content: "\f14a"; 1110 | } 1111 | .fa-pencil-square:before { 1112 | content: "\f14b"; 1113 | } 1114 | .fa-external-link-square:before { 1115 | content: "\f14c"; 1116 | } 1117 | .fa-share-square:before { 1118 | content: "\f14d"; 1119 | } 1120 | .fa-compass:before { 1121 | content: "\f14e"; 1122 | } 1123 | .fa-toggle-down:before, 1124 | .fa-caret-square-o-down:before { 1125 | content: "\f150"; 1126 | } 1127 | .fa-toggle-up:before, 1128 | .fa-caret-square-o-up:before { 1129 | content: "\f151"; 1130 | } 1131 | .fa-toggle-right:before, 1132 | .fa-caret-square-o-right:before { 1133 | content: "\f152"; 1134 | } 1135 | .fa-euro:before, 1136 | .fa-eur:before { 1137 | content: "\f153"; 1138 | } 1139 | .fa-gbp:before { 1140 | content: "\f154"; 1141 | } 1142 | .fa-dollar:before, 1143 | .fa-usd:before { 1144 | content: "\f155"; 1145 | } 1146 | .fa-rupee:before, 1147 | .fa-inr:before { 1148 | content: "\f156"; 1149 | } 1150 | .fa-cny:before, 1151 | .fa-rmb:before, 1152 | .fa-yen:before, 1153 | .fa-jpy:before { 1154 | content: "\f157"; 1155 | } 1156 | .fa-ruble:before, 1157 | .fa-rouble:before, 1158 | .fa-rub:before { 1159 | content: "\f158"; 1160 | } 1161 | .fa-won:before, 1162 | .fa-krw:before { 1163 | content: "\f159"; 1164 | } 1165 | .fa-bitcoin:before, 1166 | .fa-btc:before { 1167 | content: "\f15a"; 1168 | } 1169 | .fa-file:before { 1170 | content: "\f15b"; 1171 | } 1172 | .fa-file-text:before { 1173 | content: "\f15c"; 1174 | } 1175 | .fa-sort-alpha-asc:before { 1176 | content: "\f15d"; 1177 | } 1178 | .fa-sort-alpha-desc:before { 1179 | content: "\f15e"; 1180 | } 1181 | .fa-sort-amount-asc:before { 1182 | content: "\f160"; 1183 | } 1184 | .fa-sort-amount-desc:before { 1185 | content: "\f161"; 1186 | } 1187 | .fa-sort-numeric-asc:before { 1188 | content: "\f162"; 1189 | } 1190 | .fa-sort-numeric-desc:before { 1191 | content: "\f163"; 1192 | } 1193 | .fa-thumbs-up:before { 1194 | content: "\f164"; 1195 | } 1196 | .fa-thumbs-down:before { 1197 | content: "\f165"; 1198 | } 1199 | .fa-youtube-square:before { 1200 | content: "\f166"; 1201 | } 1202 | .fa-youtube:before { 1203 | content: "\f167"; 1204 | } 1205 | .fa-xing:before { 1206 | content: "\f168"; 1207 | } 1208 | .fa-xing-square:before { 1209 | content: "\f169"; 1210 | } 1211 | .fa-youtube-play:before { 1212 | content: "\f16a"; 1213 | } 1214 | .fa-dropbox:before { 1215 | content: "\f16b"; 1216 | } 1217 | .fa-stack-overflow:before { 1218 | content: "\f16c"; 1219 | } 1220 | .fa-instagram:before { 1221 | content: "\f16d"; 1222 | } 1223 | .fa-flickr:before { 1224 | content: "\f16e"; 1225 | } 1226 | .fa-adn:before { 1227 | content: "\f170"; 1228 | } 1229 | .fa-bitbucket:before { 1230 | content: "\f171"; 1231 | } 1232 | .fa-bitbucket-square:before { 1233 | content: "\f172"; 1234 | } 1235 | .fa-tumblr:before { 1236 | content: "\f173"; 1237 | } 1238 | .fa-tumblr-square:before { 1239 | content: "\f174"; 1240 | } 1241 | .fa-long-arrow-down:before { 1242 | content: "\f175"; 1243 | } 1244 | .fa-long-arrow-up:before { 1245 | content: "\f176"; 1246 | } 1247 | .fa-long-arrow-left:before { 1248 | content: "\f177"; 1249 | } 1250 | .fa-long-arrow-right:before { 1251 | content: "\f178"; 1252 | } 1253 | .fa-apple:before { 1254 | content: "\f179"; 1255 | } 1256 | .fa-windows:before { 1257 | content: "\f17a"; 1258 | } 1259 | .fa-android:before { 1260 | content: "\f17b"; 1261 | } 1262 | .fa-linux:before { 1263 | content: "\f17c"; 1264 | } 1265 | .fa-dribbble:before { 1266 | content: "\f17d"; 1267 | } 1268 | .fa-skype:before { 1269 | content: "\f17e"; 1270 | } 1271 | .fa-foursquare:before { 1272 | content: "\f180"; 1273 | } 1274 | .fa-trello:before { 1275 | content: "\f181"; 1276 | } 1277 | .fa-female:before { 1278 | content: "\f182"; 1279 | } 1280 | .fa-male:before { 1281 | content: "\f183"; 1282 | } 1283 | .fa-gittip:before { 1284 | content: "\f184"; 1285 | } 1286 | .fa-sun-o:before { 1287 | content: "\f185"; 1288 | } 1289 | .fa-moon-o:before { 1290 | content: "\f186"; 1291 | } 1292 | .fa-archive:before { 1293 | content: "\f187"; 1294 | } 1295 | .fa-bug:before { 1296 | content: "\f188"; 1297 | } 1298 | .fa-vk:before { 1299 | content: "\f189"; 1300 | } 1301 | .fa-weibo:before { 1302 | content: "\f18a"; 1303 | } 1304 | .fa-renren:before { 1305 | content: "\f18b"; 1306 | } 1307 | .fa-pagelines:before { 1308 | content: "\f18c"; 1309 | } 1310 | .fa-stack-exchange:before { 1311 | content: "\f18d"; 1312 | } 1313 | .fa-arrow-circle-o-right:before { 1314 | content: "\f18e"; 1315 | } 1316 | .fa-arrow-circle-o-left:before { 1317 | content: "\f190"; 1318 | } 1319 | .fa-toggle-left:before, 1320 | .fa-caret-square-o-left:before { 1321 | content: "\f191"; 1322 | } 1323 | .fa-dot-circle-o:before { 1324 | content: "\f192"; 1325 | } 1326 | .fa-wheelchair:before { 1327 | content: "\f193"; 1328 | } 1329 | .fa-vimeo-square:before { 1330 | content: "\f194"; 1331 | } 1332 | .fa-turkish-lira:before, 1333 | .fa-try:before { 1334 | content: "\f195"; 1335 | } 1336 | .fa-plus-square-o:before { 1337 | content: "\f196"; 1338 | } 1339 | -------------------------------------------------------------------------------- /src/css/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flimshaw/Valiant360/79f1b528886ab580e1624ef3b5857dbd541d62c4/src/css/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /src/css/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flimshaw/Valiant360/79f1b528886ab580e1624ef3b5857dbd541d62c4/src/css/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /src/css/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flimshaw/Valiant360/79f1b528886ab580e1624ef3b5857dbd541d62c4/src/css/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /src/css/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flimshaw/Valiant360/79f1b528886ab580e1624ef3b5857dbd541d62c4/src/css/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /src/css/valiant360.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Valiant360 panorama video player jquery plugin 3 | * 4 | * Copyright (c) 2014 Charlie Hoey <@flimshaw> 5 | * 6 | * Released under the MIT license: 7 | * http://www.opensource.org/licenses/mit-license.php 8 | */ 9 | @import 'font-awesome.less'; 10 | 11 | .transition (@transition) { 12 | -webkit-transition: @transition; 13 | -moz-transition: @transition; 14 | -ms-transition: @transition; 15 | -o-transition: @transition; 16 | } 17 | 18 | .box-sizing (@sizing) { 19 | box-sizing: @sizing; 20 | -moz-box-sizing: @sizing; 21 | -webkit-box-sizing: @sizing; 22 | } 23 | 24 | .Valiant360_default { 25 | position: relative; 26 | canvas { 27 | width: 100%; 28 | height: 100%; 29 | position: absolute; 30 | } 31 | .controlsWrapper { 32 | position: absolute; 33 | height: 0; 34 | width: 100%; 35 | bottom:0; 36 | overflow:hidden; 37 | .transition(all .5s ease-in-out); 38 | } 39 | .controls { 40 | overflow: hidden; 41 | position: absolute; 42 | bottom: 0; 43 | background-color:rgba(0,0,0,.75); 44 | height: 40px; 45 | width: 100%; 46 | .box-sizing(border-box); 47 | font-size: 50%; 48 | padding: 0 10px; 49 | .button { 50 | width: 40px; 51 | text-align: center; 52 | margin: 0; 53 | padding: 0; 54 | line-height: 40px; 55 | opacity: .5; 56 | font-size: 3em; 57 | color: #fff; 58 | text-decoration: none; 59 | .transition(all .25s ease-in-out); 60 | } 61 | .button:hover { 62 | opacity: 1; 63 | } 64 | .fullscreenButton { 65 | float: right; 66 | } 67 | .timeLabel{ 68 | color: #fff; 69 | font-size: 2em; 70 | height: 100%; 71 | line-height:40px; 72 | padding:0 10px; 73 | } 74 | 75 | .audioControl{ 76 | display: inline-block; 77 | } 78 | 79 | .audioControl:hover{ 80 | padding-right:10px; 81 | .volumeControl{ 82 | width:80px; 83 | .volumeCursor{ 84 | display:block; 85 | } 86 | } 87 | } 88 | 89 | .volumeControl:hover { 90 | opacity: 1; 91 | width:80px; 92 | } 93 | 94 | .volumeControl{ 95 | width: 0; 96 | height:26px; 97 | position:relative; 98 | top:5px; 99 | opacity: .5; 100 | display: inline-block; 101 | cursor:pointer; 102 | .transition(all .25s ease-in-out); 103 | .volumeBar{ 104 | position:absolute; 105 | top:11px; 106 | .volumeProgress { 107 | height: 4px; 108 | width: 100%; 109 | position:absolute; 110 | background-color: #fff; 111 | } 112 | .volumeCursor{ 113 | height: 12px; 114 | width: 12px; 115 | display:none; 116 | border-radius:50%; 117 | background-color: #fff; 118 | position:absolute; 119 | right:-6px; 120 | top:-4px; 121 | } 122 | } 123 | } 124 | 125 | .volumeControl:before { 126 | content:''; 127 | width:100%; 128 | background-color: #444; 129 | height: 4px; 130 | width: 100%; 131 | position:absolute; 132 | top:11px; 133 | } 134 | 135 | } 136 | .loading { 137 | position: absolute; 138 | z-index:10; 139 | height:100%; 140 | width:100%; 141 | display:none; 142 | overflow: hidden; 143 | .icon{ 144 | margin: 25% auto; 145 | width: 32px; 146 | height: 32px; 147 | font-size: 32px; 148 | color: #fff; 149 | } 150 | .waiting-icon{ 151 | text-indent: -9999em; 152 | border-radius: 50%; 153 | background: -moz-linear-gradient(left, #ffffff 10%, rgba(255, 255, 255, 0) 42%); 154 | background: -webkit-linear-gradient(left, #ffffff 10%, rgba(255, 255, 255, 0) 42%); 155 | background: -o-linear-gradient(left, #ffffff 10%, rgba(255, 255, 255, 0) 42%); 156 | background: -ms-linear-gradient(left, #ffffff 10%, rgba(255, 255, 255, 0) 42%); 157 | background: linear-gradient(to right, #ffffff 10%, rgba(255, 255, 255, 0) 42%); 158 | position: relative; 159 | -webkit-animation: load3 1.4s infinite linear; 160 | animation: valiant360_default_waiting_load 1.4s infinite linear; 161 | -webkit-transform: translateZ(0); 162 | -ms-transform: translateZ(0); 163 | transform: translateZ(0); 164 | } 165 | } 166 | .timeTooltip{ 167 | display:none; 168 | position: absolute; 169 | bottom: 50px; 170 | height: 25px; 171 | line-height: 25px; 172 | padding:5px; 173 | border-radius: 4px; 174 | background-color:rgba(0,0,0,.75); 175 | color: #fff; 176 | } 177 | } 178 | 179 | .Valiant360_default:hover { 180 | .controlsWrapper { 181 | height: 48px; 182 | } 183 | } 184 | 185 | .Valiant360_default.fullscreen { 186 | width: 100%!important; 187 | height: 100%!important; 188 | } 189 | 190 | .Valiant360_default .valiant-progress-bar { 191 | width: 100%; 192 | height: 8px; 193 | overflow: hidden; 194 | } 195 | .Valiant360_default .valiant-progress-bar:hover { 196 | cursor: pointer; 197 | } 198 | .Valiant360_default .valiant-progress-bar > div { 199 | height: 100%; 200 | float:left; 201 | background-color: #444; 202 | } 203 | .Valiant360_default .valiant-progress-bar > div:first-child { 204 | background-color: red; 205 | } 206 | 207 | .Valiant360_default .loading .waiting-icon:before { 208 | width: 50%; 209 | height: 50%; 210 | background: #ffffff; 211 | border-radius: 100% 0 0 0; 212 | position: absolute; 213 | top: 0; 214 | left: 0; 215 | content: ''; 216 | } 217 | 218 | .Valiant360_default .loading .waiting-icon:after { 219 | background: #000; 220 | width: 75%; 221 | height: 75%; 222 | border-radius: 50%; 223 | content: ''; 224 | margin: auto; 225 | position: absolute; 226 | top: 0; 227 | left: 0; 228 | bottom: 0; 229 | right: 0; 230 | } 231 | 232 | @-webkit-keyframes valiant360_default_waiting_load { 233 | 0% { 234 | -webkit-transform: rotate(0deg); 235 | transform: rotate(0deg); 236 | } 237 | 100% { 238 | -webkit-transform: rotate(360deg); 239 | transform: rotate(360deg); 240 | } 241 | } 242 | @keyframes valiant360_default_waiting_load { 243 | 0% { 244 | -webkit-transform: rotate(0deg); 245 | transform: rotate(0deg); 246 | } 247 | 100% { 248 | -webkit-transform: rotate(360deg); 249 | transform: rotate(360deg); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/lib/Detector.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author alteredq / http://alteredqualia.com/ 3 | * @author mr.doob / http://mrdoob.com/ 4 | */ 5 | 6 | var Detector = { 7 | 8 | canvas: !! window.CanvasRenderingContext2D, 9 | webgl: ( function () { try { var canvas = document.createElement( 'canvas' ); return !! window.WebGLRenderingContext && ( canvas.getContext( 'webgl' ) || canvas.getContext( 'experimental-webgl' ) ); } catch( e ) { return false; } } )(), 10 | workers: !! window.Worker, 11 | fileapi: window.File && window.FileReader && window.FileList && window.Blob, 12 | 13 | getWebGLErrorMessage: function () { 14 | 15 | var element = document.createElement( 'div' ); 16 | element.id = 'webgl-error-message'; 17 | element.style.fontFamily = 'monospace'; 18 | element.style.fontSize = '13px'; 19 | element.style.fontWeight = 'normal'; 20 | element.style.textAlign = 'center'; 21 | element.style.background = '#fff'; 22 | element.style.color = '#000'; 23 | element.style.padding = '1.5em'; 24 | element.style.width = '400px'; 25 | element.style.margin = '5em auto 0'; 26 | 27 | if ( ! this.webgl ) { 28 | 29 | element.innerHTML = window.WebGLRenderingContext ? [ 30 | 'Your graphics card does not seem to support WebGL.
', 31 | 'Find out how to get it here.' 32 | ].join( '\n' ) : [ 33 | 'Your browser does not seem to support WebGL.
', 34 | 'Find out how to get it here.' 35 | ].join( '\n' ); 36 | 37 | } 38 | 39 | return element; 40 | 41 | }, 42 | 43 | addGetWebGLMessage: function ( parameters ) { 44 | 45 | var parent, id, element; 46 | 47 | parameters = parameters || {}; 48 | 49 | parent = parameters.parent !== undefined ? parameters.parent : document.body; 50 | id = parameters.id !== undefined ? parameters.id : 'oldie'; 51 | 52 | element = Detector.getWebGLErrorMessage(); 53 | element.id = id; 54 | 55 | parent.appendChild( element ); 56 | 57 | } 58 | 59 | }; -------------------------------------------------------------------------------- /src/lib/modernizr-latest.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Modernizr v2.7.1 3 | * www.modernizr.com 4 | * 5 | * Copyright (c) Faruk Ates, Paul Irish, Alex Sexton 6 | * Available under the BSD and MIT licenses: www.modernizr.com/license/ 7 | */ 8 | 9 | /* 10 | * Modernizr tests which native CSS3 and HTML5 features are available in 11 | * the current UA and makes the results available to you in two ways: 12 | * as properties on a global Modernizr object, and as classes on the 13 | * element. This information allows you to progressively enhance 14 | * your pages with a granular level of control over the experience. 15 | * 16 | * Modernizr has an optional (not included) conditional resource loader 17 | * called Modernizr.load(), based on Yepnope.js (yepnopejs.com). 18 | * To get a build that includes Modernizr.load(), as well as choosing 19 | * which tests to include, go to www.modernizr.com/download/ 20 | * 21 | * Authors Faruk Ates, Paul Irish, Alex Sexton 22 | * Contributors Ryan Seddon, Ben Alman 23 | */ 24 | 25 | window.Modernizr = (function( window, document, undefined ) { 26 | 27 | var version = '2.7.1', 28 | 29 | Modernizr = {}, 30 | 31 | /*>>cssclasses*/ 32 | // option for enabling the HTML classes to be added 33 | enableClasses = true, 34 | /*>>cssclasses*/ 35 | 36 | docElement = document.documentElement, 37 | 38 | /** 39 | * Create our "modernizr" element that we do most feature tests on. 40 | */ 41 | mod = 'modernizr', 42 | modElem = document.createElement(mod), 43 | mStyle = modElem.style, 44 | 45 | /** 46 | * Create the input element for various Web Forms feature tests. 47 | */ 48 | inputElem /*>>inputelem*/ = document.createElement('input') /*>>inputelem*/ , 49 | 50 | /*>>smile*/ 51 | smile = ':)', 52 | /*>>smile*/ 53 | 54 | toString = {}.toString, 55 | 56 | // TODO :: make the prefixes more granular 57 | /*>>prefixes*/ 58 | // List of property values to set for css tests. See ticket #21 59 | prefixes = ' -webkit- -moz- -o- -ms- '.split(' '), 60 | /*>>prefixes*/ 61 | 62 | /*>>domprefixes*/ 63 | // Following spec is to expose vendor-specific style properties as: 64 | // elem.style.WebkitBorderRadius 65 | // and the following would be incorrect: 66 | // elem.style.webkitBorderRadius 67 | 68 | // Webkit ghosts their properties in lowercase but Opera & Moz do not. 69 | // Microsoft uses a lowercase `ms` instead of the correct `Ms` in IE8+ 70 | // erik.eae.net/archives/2008/03/10/21.48.10/ 71 | 72 | // More here: github.com/Modernizr/Modernizr/issues/issue/21 73 | omPrefixes = 'Webkit Moz O ms', 74 | 75 | cssomPrefixes = omPrefixes.split(' '), 76 | 77 | domPrefixes = omPrefixes.toLowerCase().split(' '), 78 | /*>>domprefixes*/ 79 | 80 | /*>>ns*/ 81 | ns = {'svg': 'http://www.w3.org/2000/svg'}, 82 | /*>>ns*/ 83 | 84 | tests = {}, 85 | inputs = {}, 86 | attrs = {}, 87 | 88 | classes = [], 89 | 90 | slice = classes.slice, 91 | 92 | featureName, // used in testing loop 93 | 94 | 95 | /*>>teststyles*/ 96 | // Inject element with style element and some CSS rules 97 | injectElementWithStyles = function( rule, callback, nodes, testnames ) { 98 | 99 | var style, ret, node, docOverflow, 100 | div = document.createElement('div'), 101 | // After page load injecting a fake body doesn't work so check if body exists 102 | body = document.body, 103 | // IE6 and 7 won't return offsetWidth or offsetHeight unless it's in the body element, so we fake it. 104 | fakeBody = body || document.createElement('body'); 105 | 106 | if ( parseInt(nodes, 10) ) { 107 | // In order not to give false positives we create a node for each test 108 | // This also allows the method to scale for unspecified uses 109 | while ( nodes-- ) { 110 | node = document.createElement('div'); 111 | node.id = testnames ? testnames[nodes] : mod + (nodes + 1); 112 | div.appendChild(node); 113 | } 114 | } 115 | 116 | // '].join(''); 122 | div.id = mod; 123 | // IE6 will false positive on some tests due to the style element inside the test div somehow interfering offsetHeight, so insert it into body or fakebody. 124 | // Opera will act all quirky when injecting elements in documentElement when page is served as xml, needs fakebody too. #270 125 | (body ? div : fakeBody).innerHTML += style; 126 | fakeBody.appendChild(div); 127 | if ( !body ) { 128 | //avoid crashing IE8, if background image is used 129 | fakeBody.style.background = ''; 130 | //Safari 5.13/5.1.4 OSX stops loading if ::-webkit-scrollbar is used and scrollbars are visible 131 | fakeBody.style.overflow = 'hidden'; 132 | docOverflow = docElement.style.overflow; 133 | docElement.style.overflow = 'hidden'; 134 | docElement.appendChild(fakeBody); 135 | } 136 | 137 | ret = callback(div, rule); 138 | // If this is done after page load we don't want to remove the body so check if body exists 139 | if ( !body ) { 140 | fakeBody.parentNode.removeChild(fakeBody); 141 | docElement.style.overflow = docOverflow; 142 | } else { 143 | div.parentNode.removeChild(div); 144 | } 145 | 146 | return !!ret; 147 | 148 | }, 149 | /*>>teststyles*/ 150 | 151 | /*>>mq*/ 152 | // adapted from matchMedia polyfill 153 | // by Scott Jehl and Paul Irish 154 | // gist.github.com/786768 155 | testMediaQuery = function( mq ) { 156 | 157 | var matchMedia = window.matchMedia || window.msMatchMedia; 158 | if ( matchMedia ) { 159 | return matchMedia(mq).matches; 160 | } 161 | 162 | var bool; 163 | 164 | injectElementWithStyles('@media ' + mq + ' { #' + mod + ' { position: absolute; } }', function( node ) { 165 | bool = (window.getComputedStyle ? 166 | getComputedStyle(node, null) : 167 | node.currentStyle)['position'] == 'absolute'; 168 | }); 169 | 170 | return bool; 171 | 172 | }, 173 | /*>>mq*/ 174 | 175 | 176 | /*>>hasevent*/ 177 | // 178 | // isEventSupported determines if a given element supports the given event 179 | // kangax.github.com/iseventsupported/ 180 | // 181 | // The following results are known incorrects: 182 | // Modernizr.hasEvent("webkitTransitionEnd", elem) // false negative 183 | // Modernizr.hasEvent("textInput") // in Webkit. github.com/Modernizr/Modernizr/issues/333 184 | // ... 185 | isEventSupported = (function() { 186 | 187 | var TAGNAMES = { 188 | 'select': 'input', 'change': 'input', 189 | 'submit': 'form', 'reset': 'form', 190 | 'error': 'img', 'load': 'img', 'abort': 'img' 191 | }; 192 | 193 | function isEventSupported( eventName, element ) { 194 | 195 | element = element || document.createElement(TAGNAMES[eventName] || 'div'); 196 | eventName = 'on' + eventName; 197 | 198 | // When using `setAttribute`, IE skips "unload", WebKit skips "unload" and "resize", whereas `in` "catches" those 199 | var isSupported = eventName in element; 200 | 201 | if ( !isSupported ) { 202 | // If it has no `setAttribute` (i.e. doesn't implement Node interface), try generic element 203 | if ( !element.setAttribute ) { 204 | element = document.createElement('div'); 205 | } 206 | if ( element.setAttribute && element.removeAttribute ) { 207 | element.setAttribute(eventName, ''); 208 | isSupported = is(element[eventName], 'function'); 209 | 210 | // If property was created, "remove it" (by setting value to `undefined`) 211 | if ( !is(element[eventName], 'undefined') ) { 212 | element[eventName] = undefined; 213 | } 214 | element.removeAttribute(eventName); 215 | } 216 | } 217 | 218 | element = null; 219 | return isSupported; 220 | } 221 | return isEventSupported; 222 | })(), 223 | /*>>hasevent*/ 224 | 225 | // TODO :: Add flag for hasownprop ? didn't last time 226 | 227 | // hasOwnProperty shim by kangax needed for Safari 2.0 support 228 | _hasOwnProperty = ({}).hasOwnProperty, hasOwnProp; 229 | 230 | if ( !is(_hasOwnProperty, 'undefined') && !is(_hasOwnProperty.call, 'undefined') ) { 231 | hasOwnProp = function (object, property) { 232 | return _hasOwnProperty.call(object, property); 233 | }; 234 | } 235 | else { 236 | hasOwnProp = function (object, property) { /* yes, this can give false positives/negatives, but most of the time we don't care about those */ 237 | return ((property in object) && is(object.constructor.prototype[property], 'undefined')); 238 | }; 239 | } 240 | 241 | // Adapted from ES5-shim https://github.com/kriskowal/es5-shim/blob/master/es5-shim.js 242 | // es5.github.com/#x15.3.4.5 243 | 244 | if (!Function.prototype.bind) { 245 | Function.prototype.bind = function bind(that) { 246 | 247 | var target = this; 248 | 249 | if (typeof target != "function") { 250 | throw new TypeError(); 251 | } 252 | 253 | var args = slice.call(arguments, 1), 254 | bound = function () { 255 | 256 | if (this instanceof bound) { 257 | 258 | var F = function(){}; 259 | F.prototype = target.prototype; 260 | var self = new F(); 261 | 262 | var result = target.apply( 263 | self, 264 | args.concat(slice.call(arguments)) 265 | ); 266 | if (Object(result) === result) { 267 | return result; 268 | } 269 | return self; 270 | 271 | } else { 272 | 273 | return target.apply( 274 | that, 275 | args.concat(slice.call(arguments)) 276 | ); 277 | 278 | } 279 | 280 | }; 281 | 282 | return bound; 283 | }; 284 | } 285 | 286 | /** 287 | * setCss applies given styles to the Modernizr DOM node. 288 | */ 289 | function setCss( str ) { 290 | mStyle.cssText = str; 291 | } 292 | 293 | /** 294 | * setCssAll extrapolates all vendor-specific css strings. 295 | */ 296 | function setCssAll( str1, str2 ) { 297 | return setCss(prefixes.join(str1 + ';') + ( str2 || '' )); 298 | } 299 | 300 | /** 301 | * is returns a boolean for if typeof obj is exactly type. 302 | */ 303 | function is( obj, type ) { 304 | return typeof obj === type; 305 | } 306 | 307 | /** 308 | * contains returns a boolean for if substr is found within str. 309 | */ 310 | function contains( str, substr ) { 311 | return !!~('' + str).indexOf(substr); 312 | } 313 | 314 | /*>>testprop*/ 315 | 316 | // testProps is a generic CSS / DOM property test. 317 | 318 | // In testing support for a given CSS property, it's legit to test: 319 | // `elem.style[styleName] !== undefined` 320 | // If the property is supported it will return an empty string, 321 | // if unsupported it will return undefined. 322 | 323 | // We'll take advantage of this quick test and skip setting a style 324 | // on our modernizr element, but instead just testing undefined vs 325 | // empty string. 326 | 327 | // Because the testing of the CSS property names (with "-", as 328 | // opposed to the camelCase DOM properties) is non-portable and 329 | // non-standard but works in WebKit and IE (but not Gecko or Opera), 330 | // we explicitly reject properties with dashes so that authors 331 | // developing in WebKit or IE first don't end up with 332 | // browser-specific content by accident. 333 | 334 | function testProps( props, prefixed ) { 335 | for ( var i in props ) { 336 | var prop = props[i]; 337 | if ( !contains(prop, "-") && mStyle[prop] !== undefined ) { 338 | return prefixed == 'pfx' ? prop : true; 339 | } 340 | } 341 | return false; 342 | } 343 | /*>>testprop*/ 344 | 345 | // TODO :: add testDOMProps 346 | /** 347 | * testDOMProps is a generic DOM property test; if a browser supports 348 | * a certain property, it won't return undefined for it. 349 | */ 350 | function testDOMProps( props, obj, elem ) { 351 | for ( var i in props ) { 352 | var item = obj[props[i]]; 353 | if ( item !== undefined) { 354 | 355 | // return the property name as a string 356 | if (elem === false) return props[i]; 357 | 358 | // let's bind a function 359 | if (is(item, 'function')){ 360 | // default to autobind unless override 361 | return item.bind(elem || obj); 362 | } 363 | 364 | // return the unbound function or obj or value 365 | return item; 366 | } 367 | } 368 | return false; 369 | } 370 | 371 | /*>>testallprops*/ 372 | /** 373 | * testPropsAll tests a list of DOM properties we want to check against. 374 | * We specify literally ALL possible (known and/or likely) properties on 375 | * the element including the non-vendor prefixed one, for forward- 376 | * compatibility. 377 | */ 378 | function testPropsAll( prop, prefixed, elem ) { 379 | 380 | var ucProp = prop.charAt(0).toUpperCase() + prop.slice(1), 381 | props = (prop + ' ' + cssomPrefixes.join(ucProp + ' ') + ucProp).split(' '); 382 | 383 | // did they call .prefixed('boxSizing') or are we just testing a prop? 384 | if(is(prefixed, "string") || is(prefixed, "undefined")) { 385 | return testProps(props, prefixed); 386 | 387 | // otherwise, they called .prefixed('requestAnimationFrame', window[, elem]) 388 | } else { 389 | props = (prop + ' ' + (domPrefixes).join(ucProp + ' ') + ucProp).split(' '); 390 | return testDOMProps(props, prefixed, elem); 391 | } 392 | } 393 | /*>>testallprops*/ 394 | 395 | 396 | /** 397 | * Tests 398 | * ----- 399 | */ 400 | 401 | // The *new* flexbox 402 | // dev.w3.org/csswg/css3-flexbox 403 | 404 | tests['flexbox'] = function() { 405 | return testPropsAll('flexWrap'); 406 | }; 407 | 408 | // The *old* flexbox 409 | // www.w3.org/TR/2009/WD-css3-flexbox-20090723/ 410 | 411 | tests['flexboxlegacy'] = function() { 412 | return testPropsAll('boxDirection'); 413 | }; 414 | 415 | // On the S60 and BB Storm, getContext exists, but always returns undefined 416 | // so we actually have to call getContext() to verify 417 | // github.com/Modernizr/Modernizr/issues/issue/97/ 418 | 419 | tests['canvas'] = function() { 420 | var elem = document.createElement('canvas'); 421 | return !!(elem.getContext && elem.getContext('2d')); 422 | }; 423 | 424 | tests['canvastext'] = function() { 425 | return !!(Modernizr['canvas'] && is(document.createElement('canvas').getContext('2d').fillText, 'function')); 426 | }; 427 | 428 | // webk.it/70117 is tracking a legit WebGL feature detect proposal 429 | 430 | // We do a soft detect which may false positive in order to avoid 431 | // an expensive context creation: bugzil.la/732441 432 | 433 | tests['webgl'] = function() { 434 | return !!window.WebGLRenderingContext; 435 | }; 436 | 437 | /* 438 | * The Modernizr.touch test only indicates if the browser supports 439 | * touch events, which does not necessarily reflect a touchscreen 440 | * device, as evidenced by tablets running Windows 7 or, alas, 441 | * the Palm Pre / WebOS (touch) phones. 442 | * 443 | * Additionally, Chrome (desktop) used to lie about its support on this, 444 | * but that has since been rectified: crbug.com/36415 445 | * 446 | * We also test for Firefox 4 Multitouch Support. 447 | * 448 | * For more info, see: modernizr.github.com/Modernizr/touch.html 449 | */ 450 | 451 | tests['touch'] = function() { 452 | var bool; 453 | 454 | if(('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch) { 455 | bool = true; 456 | } else { 457 | injectElementWithStyles(['@media (',prefixes.join('touch-enabled),('),mod,')','{#modernizr{top:9px;position:absolute}}'].join(''), function( node ) { 458 | bool = node.offsetTop === 9; 459 | }); 460 | } 461 | 462 | return bool; 463 | }; 464 | 465 | 466 | // geolocation is often considered a trivial feature detect... 467 | // Turns out, it's quite tricky to get right: 468 | // 469 | // Using !!navigator.geolocation does two things we don't want. It: 470 | // 1. Leaks memory in IE9: github.com/Modernizr/Modernizr/issues/513 471 | // 2. Disables page caching in WebKit: webk.it/43956 472 | // 473 | // Meanwhile, in Firefox < 8, an about:config setting could expose 474 | // a false positive that would throw an exception: bugzil.la/688158 475 | 476 | tests['geolocation'] = function() { 477 | return 'geolocation' in navigator; 478 | }; 479 | 480 | 481 | tests['postmessage'] = function() { 482 | return !!window.postMessage; 483 | }; 484 | 485 | 486 | // Chrome incognito mode used to throw an exception when using openDatabase 487 | // It doesn't anymore. 488 | tests['websqldatabase'] = function() { 489 | return !!window.openDatabase; 490 | }; 491 | 492 | // Vendors had inconsistent prefixing with the experimental Indexed DB: 493 | // - Webkit's implementation is accessible through webkitIndexedDB 494 | // - Firefox shipped moz_indexedDB before FF4b9, but since then has been mozIndexedDB 495 | // For speed, we don't test the legacy (and beta-only) indexedDB 496 | tests['indexedDB'] = function() { 497 | return !!testPropsAll("indexedDB", window); 498 | }; 499 | 500 | // documentMode logic from YUI to filter out IE8 Compat Mode 501 | // which false positives. 502 | tests['hashchange'] = function() { 503 | return isEventSupported('hashchange', window) && (document.documentMode === undefined || document.documentMode > 7); 504 | }; 505 | 506 | // Per 1.6: 507 | // This used to be Modernizr.historymanagement but the longer 508 | // name has been deprecated in favor of a shorter and property-matching one. 509 | // The old API is still available in 1.6, but as of 2.0 will throw a warning, 510 | // and in the first release thereafter disappear entirely. 511 | tests['history'] = function() { 512 | return !!(window.history && history.pushState); 513 | }; 514 | 515 | tests['draganddrop'] = function() { 516 | var div = document.createElement('div'); 517 | return ('draggable' in div) || ('ondragstart' in div && 'ondrop' in div); 518 | }; 519 | 520 | // FF3.6 was EOL'ed on 4/24/12, but the ESR version of FF10 521 | // will be supported until FF19 (2/12/13), at which time, ESR becomes FF17. 522 | // FF10 still uses prefixes, so check for it until then. 523 | // for more ESR info, see: mozilla.org/en-US/firefox/organizations/faq/ 524 | tests['websockets'] = function() { 525 | return 'WebSocket' in window || 'MozWebSocket' in window; 526 | }; 527 | 528 | 529 | // css-tricks.com/rgba-browser-support/ 530 | tests['rgba'] = function() { 531 | // Set an rgba() color and check the returned value 532 | 533 | setCss('background-color:rgba(150,255,150,.5)'); 534 | 535 | return contains(mStyle.backgroundColor, 'rgba'); 536 | }; 537 | 538 | tests['hsla'] = function() { 539 | // Same as rgba(), in fact, browsers re-map hsla() to rgba() internally, 540 | // except IE9 who retains it as hsla 541 | 542 | setCss('background-color:hsla(120,40%,100%,.5)'); 543 | 544 | return contains(mStyle.backgroundColor, 'rgba') || contains(mStyle.backgroundColor, 'hsla'); 545 | }; 546 | 547 | tests['multiplebgs'] = function() { 548 | // Setting multiple images AND a color on the background shorthand property 549 | // and then querying the style.background property value for the number of 550 | // occurrences of "url(" is a reliable method for detecting ACTUAL support for this! 551 | 552 | setCss('background:url(https://),url(https://),red url(https://)'); 553 | 554 | // If the UA supports multiple backgrounds, there should be three occurrences 555 | // of the string "url(" in the return value for elemStyle.background 556 | 557 | return (/(url\s*\(.*?){3}/).test(mStyle.background); 558 | }; 559 | 560 | 561 | 562 | // this will false positive in Opera Mini 563 | // github.com/Modernizr/Modernizr/issues/396 564 | 565 | tests['backgroundsize'] = function() { 566 | return testPropsAll('backgroundSize'); 567 | }; 568 | 569 | tests['borderimage'] = function() { 570 | return testPropsAll('borderImage'); 571 | }; 572 | 573 | 574 | // Super comprehensive table about all the unique implementations of 575 | // border-radius: muddledramblings.com/table-of-css3-border-radius-compliance 576 | 577 | tests['borderradius'] = function() { 578 | return testPropsAll('borderRadius'); 579 | }; 580 | 581 | // WebOS unfortunately false positives on this test. 582 | tests['boxshadow'] = function() { 583 | return testPropsAll('boxShadow'); 584 | }; 585 | 586 | // FF3.0 will false positive on this test 587 | tests['textshadow'] = function() { 588 | return document.createElement('div').style.textShadow === ''; 589 | }; 590 | 591 | 592 | tests['opacity'] = function() { 593 | // Browsers that actually have CSS Opacity implemented have done so 594 | // according to spec, which means their return values are within the 595 | // range of [0.0,1.0] - including the leading zero. 596 | 597 | setCssAll('opacity:.55'); 598 | 599 | // The non-literal . in this regex is intentional: 600 | // German Chrome returns this value as 0,55 601 | // github.com/Modernizr/Modernizr/issues/#issue/59/comment/516632 602 | return (/^0.55$/).test(mStyle.opacity); 603 | }; 604 | 605 | 606 | // Note, Android < 4 will pass this test, but can only animate 607 | // a single property at a time 608 | // daneden.me/2011/12/putting-up-with-androids-bullshit/ 609 | tests['cssanimations'] = function() { 610 | return testPropsAll('animationName'); 611 | }; 612 | 613 | 614 | tests['csscolumns'] = function() { 615 | return testPropsAll('columnCount'); 616 | }; 617 | 618 | 619 | tests['cssgradients'] = function() { 620 | /** 621 | * For CSS Gradients syntax, please see: 622 | * webkit.org/blog/175/introducing-css-gradients/ 623 | * developer.mozilla.org/en/CSS/-moz-linear-gradient 624 | * developer.mozilla.org/en/CSS/-moz-radial-gradient 625 | * dev.w3.org/csswg/css3-images/#gradients- 626 | */ 627 | 628 | var str1 = 'background-image:', 629 | str2 = 'gradient(linear,left top,right bottom,from(#9f9),to(white));', 630 | str3 = 'linear-gradient(left top,#9f9, white);'; 631 | 632 | setCss( 633 | // legacy webkit syntax (FIXME: remove when syntax not in use anymore) 634 | (str1 + '-webkit- '.split(' ').join(str2 + str1) + 635 | // standard syntax // trailing 'background-image:' 636 | prefixes.join(str3 + str1)).slice(0, -str1.length) 637 | ); 638 | 639 | return contains(mStyle.backgroundImage, 'gradient'); 640 | }; 641 | 642 | 643 | tests['cssreflections'] = function() { 644 | return testPropsAll('boxReflect'); 645 | }; 646 | 647 | 648 | tests['csstransforms'] = function() { 649 | return !!testPropsAll('transform'); 650 | }; 651 | 652 | 653 | tests['csstransforms3d'] = function() { 654 | 655 | var ret = !!testPropsAll('perspective'); 656 | 657 | // Webkit's 3D transforms are passed off to the browser's own graphics renderer. 658 | // It works fine in Safari on Leopard and Snow Leopard, but not in Chrome in 659 | // some conditions. As a result, Webkit typically recognizes the syntax but 660 | // will sometimes throw a false positive, thus we must do a more thorough check: 661 | if ( ret && 'webkitPerspective' in docElement.style ) { 662 | 663 | // Webkit allows this media query to succeed only if the feature is enabled. 664 | // `@media (transform-3d),(-webkit-transform-3d){ ... }` 665 | injectElementWithStyles('@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}', function( node, rule ) { 666 | ret = node.offsetLeft === 9 && node.offsetHeight === 3; 667 | }); 668 | } 669 | return ret; 670 | }; 671 | 672 | 673 | tests['csstransitions'] = function() { 674 | return testPropsAll('transition'); 675 | }; 676 | 677 | 678 | /*>>fontface*/ 679 | // @font-face detection routine by Diego Perini 680 | // javascript.nwbox.com/CSSSupport/ 681 | 682 | // false positives: 683 | // WebOS github.com/Modernizr/Modernizr/issues/342 684 | // WP7 github.com/Modernizr/Modernizr/issues/538 685 | tests['fontface'] = function() { 686 | var bool; 687 | 688 | injectElementWithStyles('@font-face {font-family:"font";src:url("https://")}', function( node, rule ) { 689 | var style = document.getElementById('smodernizr'), 690 | sheet = style.sheet || style.styleSheet, 691 | cssText = sheet ? (sheet.cssRules && sheet.cssRules[0] ? sheet.cssRules[0].cssText : sheet.cssText || '') : ''; 692 | 693 | bool = /src/i.test(cssText) && cssText.indexOf(rule.split(' ')[0]) === 0; 694 | }); 695 | 696 | return bool; 697 | }; 698 | /*>>fontface*/ 699 | 700 | // CSS generated content detection 701 | tests['generatedcontent'] = function() { 702 | var bool; 703 | 704 | injectElementWithStyles(['#',mod,'{font:0/0 a}#',mod,':after{content:"',smile,'";visibility:hidden;font:3px/1 a}'].join(''), function( node ) { 705 | bool = node.offsetHeight >= 3; 706 | }); 707 | 708 | return bool; 709 | }; 710 | 711 | 712 | 713 | // These tests evaluate support of the video/audio elements, as well as 714 | // testing what types of content they support. 715 | // 716 | // We're using the Boolean constructor here, so that we can extend the value 717 | // e.g. Modernizr.video // true 718 | // Modernizr.video.ogg // 'probably' 719 | // 720 | // Codec values from : github.com/NielsLeenheer/html5test/blob/9106a8/index.html#L845 721 | // thx to NielsLeenheer and zcorpan 722 | 723 | // Note: in some older browsers, "no" was a return value instead of empty string. 724 | // It was live in FF3.5.0 and 3.5.1, but fixed in 3.5.2 725 | // It was also live in Safari 4.0.0 - 4.0.4, but fixed in 4.0.5 726 | 727 | tests['video'] = function() { 728 | var elem = document.createElement('video'), 729 | bool = false; 730 | 731 | // IE9 Running on Windows Server SKU can cause an exception to be thrown, bug #224 732 | try { 733 | if ( bool = !!elem.canPlayType ) { 734 | bool = new Boolean(bool); 735 | bool.ogg = elem.canPlayType('video/ogg; codecs="theora"') .replace(/^no$/,''); 736 | 737 | // Without QuickTime, this value will be `undefined`. github.com/Modernizr/Modernizr/issues/546 738 | bool.h264 = elem.canPlayType('video/mp4; codecs="avc1.42E01E"') .replace(/^no$/,''); 739 | 740 | bool.webm = elem.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/^no$/,''); 741 | } 742 | 743 | } catch(e) { } 744 | 745 | return bool; 746 | }; 747 | 748 | tests['audio'] = function() { 749 | var elem = document.createElement('audio'), 750 | bool = false; 751 | 752 | try { 753 | if ( bool = !!elem.canPlayType ) { 754 | bool = new Boolean(bool); 755 | bool.ogg = elem.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,''); 756 | bool.mp3 = elem.canPlayType('audio/mpeg;') .replace(/^no$/,''); 757 | 758 | // Mimetypes accepted: 759 | // developer.mozilla.org/En/Media_formats_supported_by_the_audio_and_video_elements 760 | // bit.ly/iphoneoscodecs 761 | bool.wav = elem.canPlayType('audio/wav; codecs="1"') .replace(/^no$/,''); 762 | bool.m4a = ( elem.canPlayType('audio/x-m4a;') || 763 | elem.canPlayType('audio/aac;')) .replace(/^no$/,''); 764 | } 765 | } catch(e) { } 766 | 767 | return bool; 768 | }; 769 | 770 | 771 | // In FF4, if disabled, window.localStorage should === null. 772 | 773 | // Normally, we could not test that directly and need to do a 774 | // `('localStorage' in window) && ` test first because otherwise Firefox will 775 | // throw bugzil.la/365772 if cookies are disabled 776 | 777 | // Also in iOS5 Private Browsing mode, attempting to use localStorage.setItem 778 | // will throw the exception: 779 | // QUOTA_EXCEEDED_ERRROR DOM Exception 22. 780 | // Peculiarly, getItem and removeItem calls do not throw. 781 | 782 | // Because we are forced to try/catch this, we'll go aggressive. 783 | 784 | // Just FWIW: IE8 Compat mode supports these features completely: 785 | // www.quirksmode.org/dom/html5.html 786 | // But IE8 doesn't support either with local files 787 | 788 | tests['localstorage'] = function() { 789 | try { 790 | localStorage.setItem(mod, mod); 791 | localStorage.removeItem(mod); 792 | return true; 793 | } catch(e) { 794 | return false; 795 | } 796 | }; 797 | 798 | tests['sessionstorage'] = function() { 799 | try { 800 | sessionStorage.setItem(mod, mod); 801 | sessionStorage.removeItem(mod); 802 | return true; 803 | } catch(e) { 804 | return false; 805 | } 806 | }; 807 | 808 | 809 | tests['webworkers'] = function() { 810 | return !!window.Worker; 811 | }; 812 | 813 | 814 | tests['applicationcache'] = function() { 815 | return !!window.applicationCache; 816 | }; 817 | 818 | 819 | // Thanks to Erik Dahlstrom 820 | tests['svg'] = function() { 821 | return !!document.createElementNS && !!document.createElementNS(ns.svg, 'svg').createSVGRect; 822 | }; 823 | 824 | // specifically for SVG inline in HTML, not within XHTML 825 | // test page: paulirish.com/demo/inline-svg 826 | tests['inlinesvg'] = function() { 827 | var div = document.createElement('div'); 828 | div.innerHTML = ''; 829 | return (div.firstChild && div.firstChild.namespaceURI) == ns.svg; 830 | }; 831 | 832 | // SVG SMIL animation 833 | tests['smil'] = function() { 834 | return !!document.createElementNS && /SVGAnimate/.test(toString.call(document.createElementNS(ns.svg, 'animate'))); 835 | }; 836 | 837 | // This test is only for clip paths in SVG proper, not clip paths on HTML content 838 | // demo: srufaculty.sru.edu/david.dailey/svg/newstuff/clipPath4.svg 839 | 840 | // However read the comments to dig into applying SVG clippaths to HTML content here: 841 | // github.com/Modernizr/Modernizr/issues/213#issuecomment-1149491 842 | tests['svgclippaths'] = function() { 843 | return !!document.createElementNS && /SVGClipPath/.test(toString.call(document.createElementNS(ns.svg, 'clipPath'))); 844 | }; 845 | 846 | /*>>webforms*/ 847 | // input features and input types go directly onto the ret object, bypassing the tests loop. 848 | // Hold this guy to execute in a moment. 849 | function webforms() { 850 | /*>>input*/ 851 | // Run through HTML5's new input attributes to see if the UA understands any. 852 | // We're using f which is the element created early on 853 | // Mike Taylr has created a comprehensive resource for testing these attributes 854 | // when applied to all input types: 855 | // miketaylr.com/code/input-type-attr.html 856 | // spec: www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#input-type-attr-summary 857 | 858 | // Only input placeholder is tested while textarea's placeholder is not. 859 | // Currently Safari 4 and Opera 11 have support only for the input placeholder 860 | // Both tests are available in feature-detects/forms-placeholder.js 861 | Modernizr['input'] = (function( props ) { 862 | for ( var i = 0, len = props.length; i < len; i++ ) { 863 | attrs[ props[i] ] = !!(props[i] in inputElem); 864 | } 865 | if (attrs.list){ 866 | // safari false positive's on datalist: webk.it/74252 867 | // see also github.com/Modernizr/Modernizr/issues/146 868 | attrs.list = !!(document.createElement('datalist') && window.HTMLDataListElement); 869 | } 870 | return attrs; 871 | })('autocomplete autofocus list placeholder max min multiple pattern required step'.split(' ')); 872 | /*>>input*/ 873 | 874 | /*>>inputtypes*/ 875 | // Run through HTML5's new input types to see if the UA understands any. 876 | // This is put behind the tests runloop because it doesn't return a 877 | // true/false like all the other tests; instead, it returns an object 878 | // containing each input type with its corresponding true/false value 879 | 880 | // Big thanks to @miketaylr for the html5 forms expertise. miketaylr.com/ 881 | Modernizr['inputtypes'] = (function(props) { 882 | 883 | for ( var i = 0, bool, inputElemType, defaultView, len = props.length; i < len; i++ ) { 884 | 885 | inputElem.setAttribute('type', inputElemType = props[i]); 886 | bool = inputElem.type !== 'text'; 887 | 888 | // We first check to see if the type we give it sticks.. 889 | // If the type does, we feed it a textual value, which shouldn't be valid. 890 | // If the value doesn't stick, we know there's input sanitization which infers a custom UI 891 | if ( bool ) { 892 | 893 | inputElem.value = smile; 894 | inputElem.style.cssText = 'position:absolute;visibility:hidden;'; 895 | 896 | if ( /^range$/.test(inputElemType) && inputElem.style.WebkitAppearance !== undefined ) { 897 | 898 | docElement.appendChild(inputElem); 899 | defaultView = document.defaultView; 900 | 901 | // Safari 2-4 allows the smiley as a value, despite making a slider 902 | bool = defaultView.getComputedStyle && 903 | defaultView.getComputedStyle(inputElem, null).WebkitAppearance !== 'textfield' && 904 | // Mobile android web browser has false positive, so must 905 | // check the height to see if the widget is actually there. 906 | (inputElem.offsetHeight !== 0); 907 | 908 | docElement.removeChild(inputElem); 909 | 910 | } else if ( /^(search|tel)$/.test(inputElemType) ){ 911 | // Spec doesn't define any special parsing or detectable UI 912 | // behaviors so we pass these through as true 913 | 914 | // Interestingly, opera fails the earlier test, so it doesn't 915 | // even make it here. 916 | 917 | } else if ( /^(url|email)$/.test(inputElemType) ) { 918 | // Real url and email support comes with prebaked validation. 919 | bool = inputElem.checkValidity && inputElem.checkValidity() === false; 920 | 921 | } else { 922 | // If the upgraded input compontent rejects the :) text, we got a winner 923 | bool = inputElem.value != smile; 924 | } 925 | } 926 | 927 | inputs[ props[i] ] = !!bool; 928 | } 929 | return inputs; 930 | })('search tel url email datetime date month week time datetime-local number range color'.split(' ')); 931 | /*>>inputtypes*/ 932 | } 933 | /*>>webforms*/ 934 | 935 | 936 | // End of test definitions 937 | // ----------------------- 938 | 939 | 940 | 941 | // Run through all tests and detect their support in the current UA. 942 | // todo: hypothetically we could be doing an array of tests and use a basic loop here. 943 | for ( var feature in tests ) { 944 | if ( hasOwnProp(tests, feature) ) { 945 | // run the test, throw the return value into the Modernizr, 946 | // then based on that boolean, define an appropriate className 947 | // and push it into an array of classes we'll join later. 948 | featureName = feature.toLowerCase(); 949 | Modernizr[featureName] = tests[feature](); 950 | 951 | classes.push((Modernizr[featureName] ? '' : 'no-') + featureName); 952 | } 953 | } 954 | 955 | /*>>webforms*/ 956 | // input tests need to run. 957 | Modernizr.input || webforms(); 958 | /*>>webforms*/ 959 | 960 | 961 | /** 962 | * addTest allows the user to define their own feature tests 963 | * the result will be added onto the Modernizr object, 964 | * as well as an appropriate className set on the html element 965 | * 966 | * @param feature - String naming the feature 967 | * @param test - Function returning true if feature is supported, false if not 968 | */ 969 | Modernizr.addTest = function ( feature, test ) { 970 | if ( typeof feature == 'object' ) { 971 | for ( var key in feature ) { 972 | if ( hasOwnProp( feature, key ) ) { 973 | Modernizr.addTest( key, feature[ key ] ); 974 | } 975 | } 976 | } else { 977 | 978 | feature = feature.toLowerCase(); 979 | 980 | if ( Modernizr[feature] !== undefined ) { 981 | // we're going to quit if you're trying to overwrite an existing test 982 | // if we were to allow it, we'd do this: 983 | // var re = new RegExp("\\b(no-)?" + feature + "\\b"); 984 | // docElement.className = docElement.className.replace( re, '' ); 985 | // but, no rly, stuff 'em. 986 | return Modernizr; 987 | } 988 | 989 | test = typeof test == 'function' ? test() : test; 990 | 991 | if (typeof enableClasses !== "undefined" && enableClasses) { 992 | docElement.className += ' ' + (test ? '' : 'no-') + feature; 993 | } 994 | Modernizr[feature] = test; 995 | 996 | } 997 | 998 | return Modernizr; // allow chaining. 999 | }; 1000 | 1001 | 1002 | // Reset modElem.cssText to nothing to reduce memory footprint. 1003 | setCss(''); 1004 | modElem = inputElem = null; 1005 | 1006 | /*>>shiv*/ 1007 | /** 1008 | * @preserve HTML5 Shiv prev3.7.1 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed 1009 | */ 1010 | ;(function(window, document) { 1011 | /*jshint evil:true */ 1012 | /** version */ 1013 | var version = '3.7.0'; 1014 | 1015 | /** Preset options */ 1016 | var options = window.html5 || {}; 1017 | 1018 | /** Used to skip problem elements */ 1019 | var reSkip = /^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i; 1020 | 1021 | /** Not all elements can be cloned in IE **/ 1022 | var saveClones = /^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i; 1023 | 1024 | /** Detect whether the browser supports default html5 styles */ 1025 | var supportsHtml5Styles; 1026 | 1027 | /** Name of the expando, to work with multiple documents or to re-shiv one document */ 1028 | var expando = '_html5shiv'; 1029 | 1030 | /** The id for the the documents expando */ 1031 | var expanID = 0; 1032 | 1033 | /** Cached data for each document */ 1034 | var expandoData = {}; 1035 | 1036 | /** Detect whether the browser supports unknown elements */ 1037 | var supportsUnknownElements; 1038 | 1039 | (function() { 1040 | try { 1041 | var a = document.createElement('a'); 1042 | a.innerHTML = ''; 1043 | //if the hidden property is implemented we can assume, that the browser supports basic HTML5 Styles 1044 | supportsHtml5Styles = ('hidden' in a); 1045 | 1046 | supportsUnknownElements = a.childNodes.length == 1 || (function() { 1047 | // assign a false positive if unable to shiv 1048 | (document.createElement)('a'); 1049 | var frag = document.createDocumentFragment(); 1050 | return ( 1051 | typeof frag.cloneNode == 'undefined' || 1052 | typeof frag.createDocumentFragment == 'undefined' || 1053 | typeof frag.createElement == 'undefined' 1054 | ); 1055 | }()); 1056 | } catch(e) { 1057 | // assign a false positive if detection fails => unable to shiv 1058 | supportsHtml5Styles = true; 1059 | supportsUnknownElements = true; 1060 | } 1061 | 1062 | }()); 1063 | 1064 | /*--------------------------------------------------------------------------*/ 1065 | 1066 | /** 1067 | * Creates a style sheet with the given CSS text and adds it to the document. 1068 | * @private 1069 | * @param {Document} ownerDocument The document. 1070 | * @param {String} cssText The CSS text. 1071 | * @returns {StyleSheet} The style element. 1072 | */ 1073 | function addStyleSheet(ownerDocument, cssText) { 1074 | var p = ownerDocument.createElement('p'), 1075 | parent = ownerDocument.getElementsByTagName('head')[0] || ownerDocument.documentElement; 1076 | 1077 | p.innerHTML = 'x'; 1078 | return parent.insertBefore(p.lastChild, parent.firstChild); 1079 | } 1080 | 1081 | /** 1082 | * Returns the value of `html5.elements` as an array. 1083 | * @private 1084 | * @returns {Array} An array of shived element node names. 1085 | */ 1086 | function getElements() { 1087 | var elements = html5.elements; 1088 | return typeof elements == 'string' ? elements.split(' ') : elements; 1089 | } 1090 | 1091 | /** 1092 | * Returns the data associated to the given document 1093 | * @private 1094 | * @param {Document} ownerDocument The document. 1095 | * @returns {Object} An object of data. 1096 | */ 1097 | function getExpandoData(ownerDocument) { 1098 | var data = expandoData[ownerDocument[expando]]; 1099 | if (!data) { 1100 | data = {}; 1101 | expanID++; 1102 | ownerDocument[expando] = expanID; 1103 | expandoData[expanID] = data; 1104 | } 1105 | return data; 1106 | } 1107 | 1108 | /** 1109 | * returns a shived element for the given nodeName and document 1110 | * @memberOf html5 1111 | * @param {String} nodeName name of the element 1112 | * @param {Document} ownerDocument The context document. 1113 | * @returns {Object} The shived element. 1114 | */ 1115 | function createElement(nodeName, ownerDocument, data){ 1116 | if (!ownerDocument) { 1117 | ownerDocument = document; 1118 | } 1119 | if(supportsUnknownElements){ 1120 | return ownerDocument.createElement(nodeName); 1121 | } 1122 | if (!data) { 1123 | data = getExpandoData(ownerDocument); 1124 | } 1125 | var node; 1126 | 1127 | if (data.cache[nodeName]) { 1128 | node = data.cache[nodeName].cloneNode(); 1129 | } else if (saveClones.test(nodeName)) { 1130 | node = (data.cache[nodeName] = data.createElem(nodeName)).cloneNode(); 1131 | } else { 1132 | node = data.createElem(nodeName); 1133 | } 1134 | 1135 | // Avoid adding some elements to fragments in IE < 9 because 1136 | // * Attributes like `name` or `type` cannot be set/changed once an element 1137 | // is inserted into a document/fragment 1138 | // * Link elements with `src` attributes that are inaccessible, as with 1139 | // a 403 response, will cause the tab/window to crash 1140 | // * Script elements appended to fragments will execute when their `src` 1141 | // or `text` property is set 1142 | return node.canHaveChildren && !reSkip.test(nodeName) && !node.tagUrn ? data.frag.appendChild(node) : node; 1143 | } 1144 | 1145 | /** 1146 | * returns a shived DocumentFragment for the given document 1147 | * @memberOf html5 1148 | * @param {Document} ownerDocument The context document. 1149 | * @returns {Object} The shived DocumentFragment. 1150 | */ 1151 | function createDocumentFragment(ownerDocument, data){ 1152 | if (!ownerDocument) { 1153 | ownerDocument = document; 1154 | } 1155 | if(supportsUnknownElements){ 1156 | return ownerDocument.createDocumentFragment(); 1157 | } 1158 | data = data || getExpandoData(ownerDocument); 1159 | var clone = data.frag.cloneNode(), 1160 | i = 0, 1161 | elems = getElements(), 1162 | l = elems.length; 1163 | for(;i>shiv*/ 1309 | 1310 | // Assign private properties to the return object with prefix 1311 | Modernizr._version = version; 1312 | 1313 | // expose these for the plugin API. Look in the source for how to join() them against your input 1314 | /*>>prefixes*/ 1315 | Modernizr._prefixes = prefixes; 1316 | /*>>prefixes*/ 1317 | /*>>domprefixes*/ 1318 | Modernizr._domPrefixes = domPrefixes; 1319 | Modernizr._cssomPrefixes = cssomPrefixes; 1320 | /*>>domprefixes*/ 1321 | 1322 | /*>>mq*/ 1323 | // Modernizr.mq tests a given media query, live against the current state of the window 1324 | // A few important notes: 1325 | // * If a browser does not support media queries at all (eg. oldIE) the mq() will always return false 1326 | // * A max-width or orientation query will be evaluated against the current state, which may change later. 1327 | // * You must specify values. Eg. If you are testing support for the min-width media query use: 1328 | // Modernizr.mq('(min-width:0)') 1329 | // usage: 1330 | // Modernizr.mq('only screen and (max-width:768)') 1331 | Modernizr.mq = testMediaQuery; 1332 | /*>>mq*/ 1333 | 1334 | /*>>hasevent*/ 1335 | // Modernizr.hasEvent() detects support for a given event, with an optional element to test on 1336 | // Modernizr.hasEvent('gesturestart', elem) 1337 | Modernizr.hasEvent = isEventSupported; 1338 | /*>>hasevent*/ 1339 | 1340 | /*>>testprop*/ 1341 | // Modernizr.testProp() investigates whether a given style property is recognized 1342 | // Note that the property names must be provided in the camelCase variant. 1343 | // Modernizr.testProp('pointerEvents') 1344 | Modernizr.testProp = function(prop){ 1345 | return testProps([prop]); 1346 | }; 1347 | /*>>testprop*/ 1348 | 1349 | /*>>testallprops*/ 1350 | // Modernizr.testAllProps() investigates whether a given style property, 1351 | // or any of its vendor-prefixed variants, is recognized 1352 | // Note that the property names must be provided in the camelCase variant. 1353 | // Modernizr.testAllProps('boxSizing') 1354 | Modernizr.testAllProps = testPropsAll; 1355 | /*>>testallprops*/ 1356 | 1357 | 1358 | /*>>teststyles*/ 1359 | // Modernizr.testStyles() allows you to add custom styles to the document and test an element afterwards 1360 | // Modernizr.testStyles('#modernizr { position:absolute }', function(elem, rule){ ... }) 1361 | Modernizr.testStyles = injectElementWithStyles; 1362 | /*>>teststyles*/ 1363 | 1364 | 1365 | /*>>prefixed*/ 1366 | // Modernizr.prefixed() returns the prefixed or nonprefixed property name variant of your input 1367 | // Modernizr.prefixed('boxSizing') // 'MozBoxSizing' 1368 | 1369 | // Properties must be passed as dom-style camelcase, rather than `box-sizing` hypentated style. 1370 | // Return values will also be the camelCase variant, if you need to translate that to hypenated style use: 1371 | // 1372 | // str.replace(/([A-Z])/g, function(str,m1){ return '-' + m1.toLowerCase(); }).replace(/^ms-/,'-ms-'); 1373 | 1374 | // If you're trying to ascertain which transition end event to bind to, you might do something like... 1375 | // 1376 | // var transEndEventNames = { 1377 | // 'WebkitTransition' : 'webkitTransitionEnd', 1378 | // 'MozTransition' : 'transitionend', 1379 | // 'OTransition' : 'oTransitionEnd', 1380 | // 'msTransition' : 'MSTransitionEnd', 1381 | // 'transition' : 'transitionend' 1382 | // }, 1383 | // transEndEventName = transEndEventNames[ Modernizr.prefixed('transition') ]; 1384 | 1385 | Modernizr.prefixed = function(prop, obj, elem){ 1386 | if(!obj) { 1387 | return testPropsAll(prop, 'pfx'); 1388 | } else { 1389 | // Testing DOM property e.g. Modernizr.prefixed('requestAnimationFrame', window) // 'mozRequestAnimationFrame' 1390 | return testPropsAll(prop, obj, elem); 1391 | } 1392 | }; 1393 | /*>>prefixed*/ 1394 | 1395 | 1396 | /*>>cssclasses*/ 1397 | // Remove "no-js" class from element, if it exists: 1398 | docElement.className = docElement.className.replace(/(^|\s)no-js(\s|$)/, '$1$2') + 1399 | 1400 | // Add the new classes to the element. 1401 | (enableClasses ? ' js ' + classes.join(' ') : ''); 1402 | /*>>cssclasses*/ 1403 | 1404 | return Modernizr; 1405 | 1406 | })(this, this.document); 1407 | -------------------------------------------------------------------------------- /src/lib/require.js: -------------------------------------------------------------------------------- 1 | /* 2 | RequireJS 2.1.10 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved. 3 | Available via the MIT or new BSD license. 4 | see: http://github.com/jrburke/requirejs for details 5 | */ 6 | var requirejs,require,define; 7 | (function(ca){function G(b){return"[object Function]"===N.call(b)}function H(b){return"[object Array]"===N.call(b)}function v(b,c){if(b){var d;for(d=0;dthis.depCount&&!this.defined){if(G(c)){if(this.events.error&&this.map.isDefine||h.onError!==da)try{f=i.execCb(b,c,e,f)}catch(d){a=d}else f=i.execCb(b,c,e,f);this.map.isDefine&&void 0===f&&((e=this.module)?f=e.exports:this.usingExports&& 19 | (f=this.exports));if(a)return a.requireMap=this.map,a.requireModules=this.map.isDefine?[this.map.id]:null,a.requireType=this.map.isDefine?"define":"require",w(this.error=a)}else f=c;this.exports=f;if(this.map.isDefine&&!this.ignore&&(p[b]=f,h.onResourceLoad))h.onResourceLoad(i,this.map,this.depMaps);y(b);this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}else this.fetch()}},callPlugin:function(){var a= 20 | this.map,b=a.id,d=m(a.prefix);this.depMaps.push(d);r(d,"defined",t(this,function(f){var d,g;g=j(ba,this.map.id);var J=this.map.name,u=this.map.parentMap?this.map.parentMap.name:null,p=i.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(f.normalize&&(J=f.normalize(J,function(a){return c(a,u,!0)})||""),f=m(a.prefix+"!"+J,this.map.parentMap),r(f,"defined",t(this,function(a){this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),g=j(k,f.id)){this.depMaps.push(f); 21 | if(this.events.error)g.on("error",t(this,function(a){this.emit("error",a)}));g.enable()}}else g?(this.map.url=i.nameToUrl(g),this.load()):(d=t(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),d.error=t(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b];B(k,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&y(a.map.id)});w(a)}),d.fromText=t(this,function(f,c){var g=a.name,J=m(g),k=O;c&&(f=c);k&&(O=!1);q(J);s(l.config,b)&&(l.config[g]=l.config[b]);try{h.exec(f)}catch(j){return w(C("fromtexteval", 22 | "fromText eval for "+b+" failed: "+j,j,[b]))}k&&(O=!0);this.depMaps.push(J);i.completeLoad(g);p([g],d)}),f.load(a.name,p,d,l))}));i.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){W[this.map.id]=this;this.enabling=this.enabled=!0;v(this.depMaps,t(this,function(a,b){var c,f;if("string"===typeof a){a=m(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=j(K,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;r(a,"defined",t(this,function(a){this.defineDep(b, 23 | a);this.check()}));this.errback&&r(a,"error",t(this,this.errback))}c=a.id;f=k[c];!s(K,c)&&(f&&!f.enabled)&&i.enable(a,this)}));B(this.pluginMaps,t(this,function(a){var b=j(k,a.id);b&&!b.enabled&&i.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){v(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};i={config:l,contextName:b,registry:k,defined:p,urlFetched:T,defQueue:A,Module:$,makeModuleMap:m, 24 | nextTick:h.nextTick,onError:w,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=l.shim,c={paths:!0,bundles:!0,config:!0,map:!0};B(a,function(a,b){c[b]?(l[b]||(l[b]={}),V(l[b],a,!0,!0)):l[b]=a});a.bundles&&B(a.bundles,function(a,b){v(a,function(a){a!==b&&(ba[a]=b)})});a.shim&&(B(a.shim,function(a,c){H(a)&&(a={deps:a});if((a.exports||a.init)&&!a.exportsFn)a.exportsFn=i.makeShimExports(a);b[c]=a}),l.shim=b);a.packages&&v(a.packages,function(a){var b, 25 | a="string"===typeof a?{name:a}:a;b=a.name;a.location&&(l.paths[b]=a.location);l.pkgs[b]=a.name+"/"+(a.main||"main").replace(ja,"").replace(R,"")});B(k,function(a,b){!a.inited&&!a.map.unnormalized&&(a.map=m(b))});if(a.deps||a.callback)i.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(ca,arguments));return b||a.exports&&ea(a.exports)}},makeRequire:function(a,e){function g(f,c,d){var j,l;e.enableBuildCallback&&(c&&G(c))&&(c.__requireJsBuild= 26 | !0);if("string"===typeof f){if(G(c))return w(C("requireargs","Invalid require call"),d);if(a&&s(K,f))return K[f](k[a.id]);if(h.get)return h.get(i,f,a,g);j=m(f,a,!1,!0);j=j.id;return!s(p,j)?w(C("notloaded",'Module name "'+j+'" has not been loaded yet for context: '+b+(a?"":". Use require([])"))):p[j]}M();i.nextTick(function(){M();l=q(m(null,a));l.skipMap=e.skipMap;l.init(f,c,d,{enabled:!0});D()});return g}e=e||{};V(g,{isBrowser:z,toUrl:function(b){var e,d=b.lastIndexOf("."),g=b.split("/")[0];if(-1!== 27 | d&&(!("."===g||".."===g)||1g.attachEvent.toString().indexOf("[native code"))&&!Z?(O=!0,g.attachEvent("onreadystatechange",b.onScriptLoad)): 34 | (g.addEventListener("load",b.onScriptLoad,!1),g.addEventListener("error",b.onScriptError,!1)),g.src=d,M=g,D?y.insertBefore(g,D):y.appendChild(g),M=null,g;if(fa)try{importScripts(d),b.completeLoad(c)}catch(j){b.onError(C("importscripts","importScripts failed for "+c+" at "+d,j,[c]))}};z&&!r.skipDataMain&&U(document.getElementsByTagName("script"),function(b){y||(y=b.parentNode);if(L=b.getAttribute("data-main"))return q=L,r.baseUrl||(E=q.split("/"),q=E.pop(),Q=E.length?E.join("/")+"/":"./",r.baseUrl= 35 | Q),q=q.replace(R,""),h.jsExtRegExp.test(q)&&(q=L),r.deps=r.deps?r.deps.concat(q):[q],!0});define=function(b,c,d){var g,h;"string"!==typeof b&&(d=c,c=b,b=null);H(c)||(d=c,c=null);!c&&G(d)&&(c=[],d.length&&(d.toString().replace(la,"").replace(ma,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c)));if(O){if(!(g=M))P&&"interactive"===P.readyState||U(document.getElementsByTagName("script"),function(b){if("interactive"===b.readyState)return P=b}),g=P;g&&(b|| 36 | (b=g.getAttribute("data-requiremodule")),h=F[g.getAttribute("data-requirecontext")])}(h?h.defQueue:S).push([b,c,d])};define.amd={jQuery:!0};h.exec=function(b){return eval(b)};h(r)}})(this); 37 | -------------------------------------------------------------------------------- /src/lib/stats.min.js: -------------------------------------------------------------------------------- 1 | // stats.js - http://github.com/mrdoob/stats.js 2 | var Stats=function(){var l=Date.now(),m=l,g=0,n=Infinity,o=0,h=0,p=Infinity,q=0,r=0,s=0,f=document.createElement("div");f.id="stats";f.addEventListener("mousedown",function(b){b.preventDefault();t(++s%2)},!1);f.style.cssText="width:80px;opacity:0.9;cursor:pointer";var a=document.createElement("div");a.id="fps";a.style.cssText="padding:0 0 3px 3px;text-align:left;background-color:#002";f.appendChild(a);var i=document.createElement("div");i.id="fpsText";i.style.cssText="color:#0ff;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px"; 3 | i.innerHTML="FPS";a.appendChild(i);var c=document.createElement("div");c.id="fpsGraph";c.style.cssText="position:relative;width:74px;height:30px;background-color:#0ff";for(a.appendChild(c);74>c.children.length;){var j=document.createElement("span");j.style.cssText="width:1px;height:30px;float:left;background-color:#113";c.appendChild(j)}var d=document.createElement("div");d.id="ms";d.style.cssText="padding:0 0 3px 3px;text-align:left;background-color:#020;display:none";f.appendChild(d);var k=document.createElement("div"); 4 | k.id="msText";k.style.cssText="color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px";k.innerHTML="MS";d.appendChild(k);var e=document.createElement("div");e.id="msGraph";e.style.cssText="position:relative;width:74px;height:30px;background-color:#0f0";for(d.appendChild(e);74>e.children.length;)j=document.createElement("span"),j.style.cssText="width:1px;height:30px;float:left;background-color:#131",e.appendChild(j);var t=function(b){s=b;switch(s){case 0:a.style.display= 5 | "block";d.style.display="none";break;case 1:a.style.display="none",d.style.display="block"}};return{REVISION:11,domElement:f,setMode:t,begin:function(){l=Date.now()},end:function(){var b=Date.now();g=b-l;n=Math.min(n,g);o=Math.max(o,g);k.textContent=g+" MS ("+n+"-"+o+")";var a=Math.min(30,30-30*(g/200));e.appendChild(e.firstChild).style.height=a+"px";r++;b>m+1E3&&(h=Math.round(1E3*r/(b-m)),p=Math.min(p,h),q=Math.max(q,h),i.textContent=h+" FPS ("+p+"-"+q+")",a=Math.min(30,30-30*(h/100)),c.appendChild(c.firstChild).style.height= 6 | a+"px",m=b,r=0);return b},update:function(){l=this.end()}}}; 7 | -------------------------------------------------------------------------------- /src/valiant.jquery.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Valiant360 panorama video player/photo viewer jquery plugin 3 | * 4 | * Copyright (c) 2014 Charlie Hoey <@flimshaw> 5 | * 6 | * Released under the MIT license: 7 | * http://www.opensource.org/licenses/mit-license.php 8 | * 9 | * Jquery plugin pattern based on https://github.com/jquery-boilerplate/jquery-patterns/blob/master/patterns/jquery.basic.plugin-boilerplate.js 10 | */ 11 | 12 | /* REQUIREMENTS: 13 | 14 | jQuery 1.7.2 or greater 15 | three.js r65 or higher 16 | 17 | */ 18 | 19 | 20 | /*! 21 | * jQuery lightweight plugin boilerplate 22 | * Original author: @ajpiano 23 | * Further changes, comments: @addyosmani 24 | * Licensed under the MIT license 25 | */ 26 | 27 | // the semi-colon before the function invocation is a safety 28 | // net against concatenated scripts and/or other plugins 29 | // that are not closed properly. 30 | ;(function ( $, THREE, Detector, window, document, undefined ) { 31 | 32 | // undefined is used here as the undefined global 33 | // variable in ECMAScript 3 and is mutable (i.e. it can 34 | // be changed by someone else). undefined isn't really 35 | // being passed in so we can ensure that its value is 36 | // truly undefined. In ES5, undefined can no longer be 37 | // modified. 38 | 39 | // window and document are passed through as local 40 | // variables rather than as globals, because this (slightly) 41 | // quickens the resolution process and can be more 42 | // efficiently minified (especially when both are 43 | // regularly referenced in your plugin). 44 | 45 | // Create the defaults once 46 | var pluginName = "Valiant360", 47 | plugin, // will hold reference to instantiated Plugin 48 | defaults = { 49 | crossOrigin: 'anonymous', 50 | clickAndDrag: false, 51 | keyboardControls: true, 52 | fov: 35, 53 | fovMin: 3, 54 | fovMax: 100, 55 | hideControls: false, 56 | lon: 0, 57 | lat: 0, 58 | loop: "loop", 59 | muted: true, 60 | volume: 0.5, 61 | debug: false, 62 | flatProjection: false, 63 | autoplay: true 64 | }; 65 | 66 | // The actual plugin constructor 67 | function Plugin( element, options ) { 68 | this.element = element; 69 | 70 | // jQuery has an extend method that merges the 71 | // contents of two or more objects, storing the 72 | // result in the first object. The first object 73 | // is generally empty because we don't want to alter 74 | // the default options for future instances of the plugin 75 | this.options = $.extend( {}, defaults, options) ; 76 | 77 | this._defaults = defaults; 78 | this._name = pluginName; 79 | 80 | this.init(); 81 | } 82 | 83 | Plugin.prototype = { 84 | 85 | init: function() { 86 | // Place initialization logic here 87 | // You already have access to the DOM element and 88 | // the options via the instance, e.g. this.element 89 | // and this.options 90 | // you can add more functions like the one below and 91 | // call them like so: this.yourOtherFunction(this.element, this.options). 92 | 93 | // instantiate some local variables we're going to need 94 | this._time = new Date().getTime(); 95 | this._controls = {}; 96 | this._id = this.generateUUID(); 97 | 98 | this._requestAnimationId = ''; // used to cancel requestAnimationFrame on destroy 99 | this._isVideo = false; 100 | this._isPhoto = false; 101 | this._isFullscreen = false; 102 | this._mouseDown = false; 103 | this._dragStart = {}; 104 | 105 | this._lat = this.options.lat; 106 | this._lon = this.options.lon; 107 | this._fov = this.options.fov; 108 | 109 | // save our original height and width for returning from fullscreen 110 | this._originalWidth = $(this.element).find('canvas').width(); 111 | this._originalHeight = $(this.element).find('canvas').height(); 112 | 113 | // add a class to our element so it inherits the appropriate styles 114 | $(this.element).addClass('Valiant360_default'); 115 | 116 | // add tabindex attribute to enable the focus on the element (required for keyboard controls) 117 | if(this.options.keyboardControls && !$(this.element).attr("tabindex")){ 118 | $(this.element).attr("tabindex", "1"); 119 | } 120 | 121 | this.createMediaPlayer(); 122 | this.createControls(); 123 | 124 | }, 125 | 126 | generateUUID: function(){ 127 | var d = new Date().getTime(); 128 | var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 129 | var r = (d + Math.random()*16)%16 | 0; 130 | d = Math.floor(d/16); 131 | return (c==='x' ? r : (r&0x7|0x8)).toString(16); 132 | }); 133 | return uuid; 134 | }, 135 | 136 | createMediaPlayer: function() { 137 | 138 | // make a self reference we can pass to our callbacks 139 | var self = this; 140 | 141 | // create a local THREE.js scene 142 | this._scene = new THREE.Scene(); 143 | 144 | // create ThreeJS camera 145 | this._camera = new THREE.PerspectiveCamera(this._fov, $(this.element).width() / $(this.element).height(), 0.1, 1000); 146 | this._camera.setLens(this._fov); 147 | 148 | // create ThreeJS renderer and append it to our object 149 | this._renderer = Detector.webgl? new THREE.WebGLRenderer(): new THREE.CanvasRenderer(); 150 | this._renderer.setSize( $(this.element).width(), $(this.element).height() ); 151 | this._renderer.autoClear = false; 152 | this._renderer.setClearColor( 0x333333, 1 ); 153 | 154 | // append the rendering element to this div 155 | $(this.element).append(this._renderer.domElement); 156 | 157 | var createAnimation = function () { 158 | self._texture.generateMipmaps = false; 159 | self._texture.minFilter = THREE.LinearFilter; 160 | self._texture.magFilter = THREE.LinearFilter; 161 | self._texture.format = THREE.RGBFormat; 162 | 163 | // create ThreeJS mesh sphere onto which our texture will be drawn 164 | self._mesh = new THREE.Mesh( new THREE.SphereGeometry( 500, 80, 50 ), new THREE.MeshBasicMaterial( { map: self._texture } ) ); 165 | self._mesh.scale.x = -1; // mirror the texture, since we're looking from the inside out 166 | self._scene.add(self._mesh); 167 | 168 | self.animate(); 169 | }; 170 | 171 | // figure out our texturing situation, based on what our source is 172 | if( $(this.element).attr('data-photo-src') ) { 173 | this._isPhoto = true; 174 | THREE.ImageUtils.crossOrigin = this.options.crossOrigin; 175 | this._texture = THREE.ImageUtils.loadTexture( $(this.element).attr('data-photo-src') ); 176 | createAnimation(); 177 | } else { 178 | this._isVideo = true; 179 | 180 | // create loading overlay 181 | var loadingHTML = '
\ 182 |
\ 183 |
\ 184 |
'; 185 | $(this.element).append(loadingHTML); 186 | this.showWaiting(); 187 | 188 | // create off-dom video player 189 | this._video = document.createElement( 'video' ); 190 | this._video.setAttribute('crossorigin', this.options.crossOrigin); 191 | this._video.style.display = 'none'; 192 | $(this.element).append( this._video ); 193 | this._video.loop = this.options.loop; 194 | this._video.muted = this.options.muted; 195 | this._video.volume = this.options.volume; 196 | 197 | // attach video player event listeners 198 | this._video.addEventListener("ended", function() { 199 | 200 | }); 201 | 202 | // Progress Meter 203 | this._video.addEventListener("progress", function() { 204 | var percent = null; 205 | if (self._video && self._video.buffered && self._video.buffered.length > 0 && self._video.buffered.end && self._video.duration) { 206 | percent = self._video.buffered.end(0) / self._video.duration; 207 | } 208 | // Some browsers (e.g., FF3.6 and Safari 5) cannot calculate target.bufferered.end() 209 | // to be anything other than 0. If the byte count is available we use this instead. 210 | // Browsers that support the else if do not seem to have the bufferedBytes value and 211 | // should skip to there. Tested in Safari 5, Webkit head, FF3.6, Chrome 6, IE 7/8. 212 | else if (self._video && self._video.bytesTotal !== undefined && self._video.bytesTotal > 0 && self._video.bufferedBytes !== undefined) { 213 | percent = self._video.bufferedBytes / self._video.bytesTotal; 214 | } 215 | 216 | // Someday we can have a loading animation for videos 217 | var cpct = Math.round(percent * 100); 218 | if(cpct === 100) { 219 | // do something now that we are done 220 | } else { 221 | // do something with this percentage info (cpct) 222 | } 223 | }); 224 | // Error listener 225 | this._video.addEventListener('error', function (event) { 226 | console.error(self._video.error); 227 | self.showError(); 228 | }); 229 | 230 | this._video.addEventListener("timeupdate", function() { 231 | if (this.paused === false){ 232 | var percent = this.currentTime * 100 / this.duration; 233 | $(self.element).find('.controlsWrapper > .valiant-progress-bar')[0].children[0].setAttribute("style", "width:" + percent + "%;"); 234 | $(self.element).find('.controlsWrapper > .valiant-progress-bar')[0].children[1].setAttribute("style", "width:" + (100 - percent) + "%;"); 235 | //Update time label 236 | var durMin = Math.floor(this.duration / 60); 237 | var durSec = Math.floor(this.duration - (durMin * 60)); 238 | var timeMin = Math.floor(this.currentTime / 60); 239 | var timeSec = Math.floor(this.currentTime - (timeMin * 60)); 240 | var duration = durMin + ':' + (durSec < 10 ? '0' + durSec : durSec); 241 | var currentTime = timeMin + ':' + (timeSec < 10 ? '0' + timeSec : timeSec); 242 | $(self.element).find('.controls .timeLabel').html(currentTime+' / '+duration); 243 | } 244 | }); 245 | 246 | // IE 11 and previous not supports THREE.Texture([video]), we must create a canvas that draws the video and use that to create the Texture 247 | var isIE = navigator.appName == 'Microsoft Internet Explorer' || !!(navigator.userAgent.match(/Trident/) || navigator.userAgent.match(/rv 11/)); 248 | if (isIE) { 249 | this._videocanvas = document.createElement('canvas'); 250 | this._texture = new THREE.Texture(this._videocanvas); 251 | // set canvas size = video size when known 252 | this._video.addEventListener('loadedmetadata', function () { 253 | self._videocanvas.width = self._video.videoWidth; 254 | self._videocanvas.height = self._video.videoHeight; 255 | createAnimation(); 256 | }); 257 | }else{ 258 | this._texture = new THREE.Texture( this._video ); 259 | } 260 | 261 | //force browser caching of the video to solve rendering errors with big videos 262 | var xhr = new XMLHttpRequest(); 263 | xhr.open('GET', $(this.element).attr('data-video-src'), true); 264 | xhr.responseType = 'blob'; 265 | xhr.onload = function (e) { 266 | if (this.status === 200) { 267 | var vid = (window.webkitURL ? webkitURL : URL).createObjectURL(this.response); 268 | //Video Play Listener, fires after video loads 269 | $(self._video).bind("canplaythrough", function () { 270 | if (self.options.autoplay === true) { 271 | self.hideWaiting(); 272 | self.play(); 273 | self._videoReady = true; 274 | } 275 | }); 276 | 277 | // set the video src and begin loading 278 | self._video.src = vid; 279 | } 280 | }; 281 | xhr.onreadystatechange = function (oEvent) { 282 | if (xhr.readyState === 4) { 283 | if (xhr.status !== 200) { 284 | console.error('Video error: status ' + xhr.status); 285 | self.showError(); 286 | } 287 | } 288 | }; 289 | xhr.send(); 290 | 291 | if(!isIE){ 292 | createAnimation(); 293 | } 294 | } 295 | }, 296 | 297 | // creates div and buttons for onscreen video controls 298 | createControls: function() { 299 | 300 | var muteControl = this.options.muted ? 'fa-volume-off' : 'fa-volume-up'; 301 | var playPauseControl = this.options.autoplay ? 'fa-pause' : 'fa-play'; 302 | 303 | var controlsHTML = ' \ 304 |
\ 305 |
\ 306 |
\ 307 |
\ 308 |
\ 309 | \ 310 |
\ 311 | \ 312 |
\ 313 |
\ 314 |
\ 315 |
\ 316 |
\ 317 |
\ 318 |
\ 319 | \ 320 | \ 321 |
\ 322 |
\ 323 | '; 324 | 325 | $(this.element).append(controlsHTML, true); 326 | $(this.element).append('
00:00
', true); 327 | 328 | // hide controls if option is set 329 | if(this.options.hideControls) { 330 | $(this.element).find('.controls').hide(); 331 | } 332 | 333 | // wire up controller events to dom elements 334 | this.attachControlEvents(); 335 | }, 336 | 337 | attachControlEvents: function() { 338 | 339 | // create a self var to pass to our controller functions 340 | var self = this; 341 | 342 | this.element.addEventListener( 'mousemove', this.onMouseMove.bind(this), false ); 343 | this.element.addEventListener( 'touchmove', this.onMouseMove.bind(this), false ); 344 | this.element.addEventListener( 'mousewheel', this.onMouseWheel.bind(this), false ); 345 | this.element.addEventListener( 'DOMMouseScroll', this.onMouseWheel.bind(this), false ); 346 | this.element.addEventListener( 'mousedown', this.onMouseDown.bind(this), false); 347 | this.element.addEventListener( 'touchstart', this.onMouseDown.bind(this), false); 348 | this.element.addEventListener( 'mouseup', this.onMouseUp.bind(this), false); 349 | this.element.addEventListener( 'touchend', this.onMouseUp.bind(this), false); 350 | 351 | if(this.options.keyboardControls){ 352 | this.element.addEventListener('keydown',this.onKeyDown.bind(this), false); 353 | this.element.addEventListener('keyup',this.onKeyUp.bind(this), false); 354 | // Used custom press event because for the arrow buttons is not throws the 'keypress' event 355 | this.element.addEventListener('keyArrowPress',this.onKeyArrowPress.bind(this), false); 356 | this.element.addEventListener('click',function () { 357 | $(self.element).focus(); 358 | },false); 359 | } 360 | 361 | $(self.element).find('.controlsWrapper > .valiant-progress-bar')[0].addEventListener("click", this.onProgressClick.bind(this), false); 362 | $(self.element).find('.controlsWrapper > .valiant-progress-bar')[0].addEventListener("mousemove", this.onProgressMouseMove.bind(this), false); 363 | $(self.element).find('.controlsWrapper > .valiant-progress-bar')[0].addEventListener("mouseout", this.onProgressMouseOut.bind(this), false); 364 | 365 | $(document).on('webkitfullscreenchange mozfullscreenchange fullscreenchange MSFullscreenChange',this.fullscreen.bind(this)); 366 | 367 | $(window).resize(function() { 368 | self.resizeGL($(self.element).width(), $(self.element).height()); 369 | }); 370 | 371 | // Player Controls 372 | $(this.element).find('.playButton').click(function(e) { 373 | e.preventDefault(); 374 | if($(this).hasClass('fa-pause')) { 375 | $(this).removeClass('fa-pause').addClass('fa-play'); 376 | self.pause(); 377 | } else { 378 | $(this).removeClass('fa-play').addClass('fa-pause'); 379 | self.play(); 380 | } 381 | }); 382 | 383 | $(this.element).find(".fullscreenButton").click(function(e) { 384 | e.preventDefault(); 385 | var elem = $(self.element)[0]; 386 | if($(this).hasClass('fa-expand')) { 387 | if (elem.requestFullscreen) { 388 | elem.requestFullscreen(); 389 | } else if (elem.msRequestFullscreen) { 390 | elem.msRequestFullscreen(); 391 | } else if (elem.mozRequestFullScreen) { 392 | elem.mozRequestFullScreen(); 393 | } else if (elem.webkitRequestFullscreen) { 394 | elem.webkitRequestFullscreen(); 395 | } 396 | } else { 397 | if (elem.requestFullscreen) { 398 | document.exitFullscreen(); 399 | } else if (elem.msRequestFullscreen) { 400 | document.msExitFullscreen(); 401 | } else if (elem.mozRequestFullScreen) { 402 | document.mozCancelFullScreen(); 403 | } else if (elem.webkitRequestFullscreen) { 404 | document.webkitExitFullscreen(); 405 | } 406 | } 407 | }); 408 | 409 | $(this.element).find(".muteButton").click(function(e) { 410 | e.preventDefault(); 411 | if($(this).hasClass('fa-volume-off')) { 412 | $(this).removeClass('fa-volume-off').addClass('fa-volume-up'); 413 | self._video.muted = false; 414 | } else { 415 | $(this).removeClass('fa-volume-up').addClass('fa-volume-off'); 416 | self._video.muted = true; 417 | } 418 | }); 419 | 420 | $(this.element).find('.controlsWrapper .volumeControl') 421 | .mousedown(this.onVolumeMouseDown.bind(this)) 422 | .mouseup(this.onVolumeMouseUp.bind(this)) 423 | .mouseleave(this.onVolumeMouseUp.bind(this)) 424 | .mousemove(this.onVolumeMouseMove.bind(this)); 425 | 426 | $(this._video).on('volumechange',this.onVolumeChange.bind(this)); 427 | }, 428 | 429 | onMouseMove: function(event) { 430 | this._onPointerDownPointerX = event.clientX; 431 | this._onPointerDownPointerY = -event.clientY; 432 | 433 | this.relativeX = event.pageX - $(this.element).find('canvas').offset().left; 434 | 435 | this._onPointerDownLon = this._lon; 436 | this._onPointerDownLat = this._lat; 437 | 438 | var x, y; 439 | 440 | if(this.options.clickAndDrag) { 441 | if(this._mouseDown) { 442 | x = event.pageX - this._dragStart.x; 443 | y = event.pageY - this._dragStart.y; 444 | this._dragStart.x = event.pageX; 445 | this._dragStart.y = event.pageY; 446 | this._lon += x; 447 | this._lat -= y; 448 | } 449 | } else { 450 | x = event.pageX - $(this.element).find('canvas').offset().left; 451 | y = event.pageY - $(this.element).find('canvas').offset().top; 452 | this._lon = ( x / $(this.element).find('canvas').width() ) * 430 - 225; 453 | this._lat = ( y / $(this.element).find('canvas').height() ) * -180 + 90; 454 | } 455 | }, 456 | 457 | onMouseWheel: function(event) { 458 | 459 | var wheelSpeed = -0.01; 460 | 461 | // WebKit 462 | if ( event.wheelDeltaY ) { 463 | this._fov -= event.wheelDeltaY * wheelSpeed; 464 | // Opera / Explorer 9 465 | } else if ( event.wheelDelta ) { 466 | this._fov -= event.wheelDelta * wheelSpeed; 467 | // Firefox 468 | } else if ( event.detail ) { 469 | this._fov += event.detail * 1.0; 470 | } 471 | 472 | if(this._fov < this.options.fovMin) { 473 | this._fov = this.options.fovMin; 474 | } else if(this._fov > this.options.fovMax) { 475 | this._fov = this.options.fovMax; 476 | } 477 | 478 | this._camera.setLens(this._fov); 479 | event.preventDefault(); 480 | }, 481 | 482 | onMouseDown: function(event) { 483 | this._mouseDown = true; 484 | this._dragStart.x = event.pageX; 485 | this._dragStart.y = event.pageY; 486 | }, 487 | 488 | onProgressClick: function(event) { 489 | if(this._isVideo && this._video.readyState === this._video.HAVE_ENOUGH_DATA) { 490 | var percent = this.relativeX / $(this.element).find('canvas').width() * 100; 491 | $(this.element).find('.controlsWrapper > .valiant-progress-bar')[0].children[0].setAttribute("style", "width:" + percent + "%;"); 492 | $(this.element).find('.controlsWrapper > .valiant-progress-bar')[0].children[1].setAttribute("style", "width:" + (100 - percent) + "%;"); 493 | this._video.currentTime = this._video.duration * percent / 100; 494 | } 495 | }, 496 | 497 | onProgressMouseMove: function(event){ 498 | var percent = this.relativeX / $(this.element).find('canvas').width() * 100; 499 | if(percent){ 500 | var tooltip = $(this.element).find('.timeTooltip'); 501 | var tooltipLeft = (this.relativeX - (tooltip.width()/2)); 502 | tooltipLeft = tooltipLeft <0? 0:Math.min(tooltipLeft,$(this.element).find('canvas').width() - tooltip.outerWidth()); 503 | tooltip.css({ left: tooltipLeft + 'px' }); 504 | tooltip.show(); 505 | var time = (percent / 100) * this._video.duration; 506 | var timeMin = Math.floor(time / 60); 507 | var timeSec = Math.floor(time - (timeMin * 60)); 508 | tooltip.html(timeMin + ':' + (timeSec < 10 ? '0' + timeSec : timeSec)); 509 | } 510 | }, 511 | 512 | onProgressMouseOut: function (event) { 513 | $(this.element).find('.timeTooltip').hide(); 514 | }, 515 | 516 | onMouseUp: function(event) { 517 | this._mouseDown = false; 518 | }, 519 | 520 | onKeyDown: function(event) { 521 | var keyCode = event.keyCode; 522 | if (keyCode >= 37 && keyCode <= 40) { 523 | event.preventDefault(); 524 | this._keydown = true; 525 | var pressEvent = document.createEvent('CustomEvent'); 526 | pressEvent.initCustomEvent("keyArrowPress",true,true,{'keyCode':keyCode}); 527 | this.element.dispatchEvent(pressEvent); 528 | } 529 | }, 530 | 531 | onKeyUp: function (event) { 532 | var keyCode = event.keyCode; 533 | if (keyCode >= 37 && keyCode <= 40) { 534 | event.preventDefault(); 535 | this._keydown = false; 536 | } 537 | }, 538 | 539 | onKeyArrowPress: function (event) { 540 | if (this._keydown) { 541 | var keyCode = event.detail? event.detail.keyCode:null; 542 | var offset = 3; 543 | var pressDelay = 50; 544 | var element = this.element; 545 | event.preventDefault(); 546 | switch (keyCode) { 547 | //Arrow left 548 | case 37: this._lon -= offset; 549 | break; 550 | //Arrow right 551 | case 39: this._lon += offset; 552 | break; 553 | //Arrow up 554 | case 38: this._lat += offset; 555 | break; 556 | //Arrow down 557 | case 40: this._lat -= offset; 558 | break; 559 | } 560 | setTimeout(function () { 561 | var pressEvent = document.createEvent('CustomEvent'); 562 | pressEvent.initCustomEvent("keyArrowPress",true,true,{'keyCode':keyCode}); 563 | element.dispatchEvent(pressEvent); 564 | }, 565 | pressDelay); 566 | } 567 | }, 568 | 569 | onVolumeMouseDown: function(event){ 570 | event.preventDefault(); 571 | this._volumeMouseDown = true; 572 | this.onVolumeMouseMove(event); 573 | }, 574 | 575 | onVolumeMouseUp: function(event){ 576 | event.preventDefault(); 577 | this._volumeMouseDown = false; 578 | }, 579 | 580 | onVolumeMouseMove: function(event){ 581 | event.preventDefault(); 582 | if(this._volumeMouseDown){ 583 | var volumeControl = $(this.element).find('.controlsWrapper .volumeControl'); 584 | var percent = (this.relativeX - volumeControl.offset().left + (volumeControl.find('.volumeBar > .volumeCursor').width()/2)) / volumeControl.width() * 100; 585 | if(percent>=0 && percent<=100){ 586 | this._video.volume = percent/100; 587 | } 588 | } 589 | }, 590 | 591 | onVolumeChange: function(event){ 592 | //change volume cursor value 593 | var percent = this._video.muted==true && !this._volumeMouseDown? 0:(this._video.volume * 100); 594 | $(this.element).find('.controlsWrapper .volumeControl > .volumeBar').css({width: percent+"%"}); 595 | 596 | //change mute button 597 | var muteButton = $(this.element).find(".muteButton"); 598 | if((percent>0 && muteButton.hasClass('fa-volume-off')) || (percent==0 && muteButton.hasClass('fa-volume-up'))){ 599 | muteButton.click(); 600 | } 601 | }, 602 | 603 | animate: function() { 604 | // set our animate function to fire next time a frame is ready 605 | this._requestAnimationId = requestAnimationFrame( this.animate.bind(this) ); 606 | 607 | if( this._isVideo ) { 608 | if ( this._video.readyState === this._video.HAVE_ENOUGH_DATA) { 609 | if(this._videocanvas) { 610 | this._videocanvas.getContext('2d').drawImage(this._video, 0, 0, this._videocanvas.width, this._videocanvas.height); 611 | } 612 | if(typeof(this._texture) !== "undefined" ) { 613 | var ct = new Date().getTime(); 614 | if(ct - this._time >= 30) { 615 | this._texture.needsUpdate = true; 616 | this._time = ct; 617 | } 618 | } 619 | } 620 | } 621 | 622 | this.render(); 623 | }, 624 | 625 | render: function() { 626 | this._lat = Math.max( - 85, Math.min( 85, this._lat ) ); 627 | this._phi = ( 90 - this._lat ) * Math.PI / 180; 628 | this._theta = this._lon * Math.PI / 180; 629 | 630 | var cx = 500 * Math.sin( this._phi ) * Math.cos( this._theta ); 631 | var cy = 500 * Math.cos( this._phi ); 632 | var cz = 500 * Math.sin( this._phi ) * Math.sin( this._theta ); 633 | 634 | this._camera.lookAt(new THREE.Vector3(cx, cy, cz)); 635 | 636 | // distortion 637 | if(this.options.flatProjection) { 638 | this._camera.position.x = 0; 639 | this._camera.position.y = 0; 640 | this._camera.position.z = 0; 641 | } else { 642 | this._camera.position.x = - cx; 643 | this._camera.position.y = - cy; 644 | this._camera.position.z = - cz; 645 | } 646 | 647 | this._renderer.clear(); 648 | this._renderer.render( this._scene, this._camera ); 649 | }, 650 | 651 | // Video specific functions, exposed to controller 652 | play: function() { 653 | //code to play media 654 | this._video.play(); 655 | }, 656 | 657 | pause: function() { 658 | //code to stop media 659 | this._video.pause(); 660 | }, 661 | 662 | loadVideo: function(videoFile) { 663 | this._video.src = videoFile; 664 | }, 665 | unloadVideo: function() { 666 | // overkill unloading to avoid dreaded video 'pending' bug in Chrome. See https://code.google.com/p/chromium/issues/detail?id=234779 667 | this.pause(); 668 | this._video.src = ''; 669 | this._video.removeAttribute('src'); 670 | }, 671 | loadPhoto: function(photoFile) { 672 | this._texture = THREE.ImageUtils.loadTexture( photoFile ); 673 | }, 674 | 675 | fullscreen: function() { 676 | if($(this.element).find('a.fa-expand').length > 0) { 677 | this.resizeGL(screen.width, screen.height); 678 | 679 | $(this.element).addClass('fullscreen'); 680 | $(this.element).find('a.fa-expand').removeClass('fa-expand').addClass('fa-compress'); 681 | 682 | this._isFullscreen = true; 683 | } else { 684 | this.resizeGL(this._originalWidth, this._originalHeight); 685 | 686 | $(this.element).removeClass('fullscreen'); 687 | $(this.element).find('a.fa-compress').removeClass('fa-compress').addClass('fa-expand'); 688 | 689 | this._isFullscreen = false; 690 | } 691 | }, 692 | 693 | resizeGL: function(w, h) { 694 | this._renderer.setSize(w, h); 695 | this._camera.aspect = w / h; 696 | this._camera.updateProjectionMatrix(); 697 | }, 698 | 699 | showWaiting: function () { 700 | var loading = $(this.element).find('.loading'); 701 | loading.find('.waiting-icon').show(); 702 | loading.find('.error-icon').hide(); 703 | loading.show(); 704 | }, 705 | 706 | hideWaiting: function () { 707 | $(this.element).find('.loading').hide(); 708 | }, 709 | 710 | showError: function () { 711 | var loading = $(this.element).find('.loading'); 712 | loading.find('.waiting-icon').hide(); 713 | loading.find('.error-icon').show(); 714 | loading.show(); 715 | }, 716 | 717 | destroy: function() { 718 | window.cancelAnimationFrame(this._requestAnimationId); 719 | this._requestAnimationId = ''; 720 | this._texture.dispose(); 721 | this._scene.remove(this._mesh); 722 | if(this._isVideo) { 723 | this.unloadVideo(); 724 | } 725 | $(this._renderer.domElement).remove(); 726 | } 727 | }; 728 | 729 | $.fn[pluginName] = function ( options ) { 730 | return this.each(function () { 731 | if(typeof options === 'object' || !options) { 732 | // A really lightweight plugin wrapper around the constructor, 733 | // preventing against multiple instantiations 734 | this.plugin = new Plugin(this, options); 735 | if (!$.data(this, "plugin_" + pluginName)) { 736 | $.data(this, "plugin_" + pluginName, this.plugin); 737 | } 738 | } else if(this.plugin[options]) { 739 | // Allows plugin methods to be called 740 | return this.plugin[options].apply(this.plugin, Array.prototype.slice.call(arguments, 1)) 741 | } 742 | }); 743 | }; 744 | 745 | })( jQuery, THREE, Detector, window, document ); 746 | --------------------------------------------------------------------------------