├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── tests.yaml ├── .gitignore ├── .npmignore ├── .prettierignore ├── CHANGES.md ├── FUNDING.yml ├── LICENSE.md ├── README.md ├── docs ├── core.md ├── custom.md └── extra.md ├── examples ├── basic_cube.html ├── basic_cube.png ├── controls_firstperson_cube.html ├── controls_orbit_cube.html ├── element_cube.html ├── empty.html ├── multiple_renderers.html ├── multiple_threestraps.html ├── tooltip.html ├── vr.html └── vr.png ├── gulpfile.js ├── karma.conf.ts ├── package-lock.json ├── package.json ├── src ├── aliases.js ├── api.js ├── binder.js ├── bootstrap.js ├── controls │ ├── VRControls.js │ └── index.js ├── core │ ├── bind.js │ ├── camera.js │ ├── fallback.js │ ├── fill.js │ ├── index.js │ ├── loop.js │ ├── render.js │ ├── renderer.js │ ├── scene.js │ ├── size.js │ ├── time.js │ └── warmup.js ├── extra │ ├── controls.js │ ├── cursor.js │ ├── fullscreen.js │ ├── index.js │ ├── stats.js │ ├── ui.js │ └── vr.js ├── index.js └── renderers │ ├── MultiRenderer.js │ ├── VRRenderer.js │ └── index.js ├── test ├── api.spec.js ├── binder.spec.js ├── bootstrap.spec.js ├── core │ ├── bind.spec.js │ ├── camera.spec.js │ ├── fallback.spec.js │ ├── fill.spec.js │ ├── loop.spec.js │ ├── render.spec.js │ ├── renderer.spec.js │ ├── scene.spec.js │ ├── size.spec.js │ ├── time.spec.js │ └── warmup.spec.js ├── extra │ ├── controls.spec.js │ ├── cursor.spec.js │ ├── fullscreen.spec.js │ ├── stats.spec.js │ └── vr.spec.js └── plugin.spec.js ├── tsconfig.json └── webpack.config.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | build/* -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ["jasmine"], 3 | env: { 4 | browser: true, 5 | es2021: true, 6 | jasmine: true, 7 | node: true, 8 | }, 9 | extends: "eslint:recommended", 10 | parserOptions: { 11 | ecmaVersion: 12, 12 | sourceType: "module", 13 | }, 14 | rules: { 15 | "no-var": "warn", 16 | "no-unused-vars": ["error", { argsIgnorePattern: "^_" }], 17 | "no-multi-str": 1, 18 | "prefer-const": "warn" 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [master] 7 | 8 | jobs: 9 | tests: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Use Node.js 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version: '18.x' 17 | - run: npm ci --include=dev 18 | - run: npm run lint 19 | - run: npm test 20 | - run: npm run build -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | build 4 | build_tests 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | **/webpack.config.js 3 | node_modules 4 | docs 5 | examples 6 | karma.conf.js 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage 4 | docs 5 | vendor 6 | .* 7 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## Changes 2 | 3 | ### 0.5.1 4 | - Changed how Threestrap imports ThreeJS. Previously, Threestrap (usually) imported from `three/src`. Now it consistently imports from `three`. This change should generally not affect users unless they were using `instanceof` checks. 5 | - Fixed tests for CI [#28](https://github.com/unconed/threestrap/pull/28) 6 | 7 | ### 0.5.0 8 | - THREE.js is no longer included in the build output and must be included separately. [#26](https://github.com/unconed/threestrap/pull/26) 9 | - Fixed stats plugin throwing an error on destroy() if mounted in a non-body element [#25](https://github.com/unconed/threestrap/pull/25) 10 | 11 | ### 0.0.12-dev 12 | 13 | - Camera: Lower near distance to 0.01 by default. 14 | - Controls: Ship TrackballControls. 15 | - Loop: Add `each` option for rendering at (integer) reduced frame rates. 16 | 17 | ### 0.0.11 18 | 19 | - Time: Provide `warmup` option to wait N frames before starting clock (complements render warmup) 20 | - Time: Add `time` real time clock. 21 | 22 | ### 0.0.10 23 | 24 | - Bootstrap: Can pass in CSS selector for { element: ... } option. 25 | - Size: handle retina/high-DPI displays to match Three.js changes. 26 | 27 | ### 0.0.9 28 | 29 | - New Extra/VR: `render` replacement for Oculus/Cardboard VR using the `getVRDevices` API and `THREE.VRRenderer`. Specify as `render:vr` or use the `VR` alias, see `examples/vr.html`. 30 | - New Extra/Fullscreen: Go fullscreen / VR with keypress or API. 31 | - New Extra/UI: Minimal UI controls for fullscreen / VR. 32 | - Extra/Controls: Pass VR state to controls. 33 | - Extra/Cursor: Hide cursor in VR mode. 34 | - Vendor/controls/VRControls: Apply VR state and fallback to DeviceOrientationControls / OrbitControls 35 | 36 | ### 0.0.8 37 | 38 | - New Core/Warmup: Hide canvas for first few frames to avoid stuttering while JS/GL warms up. 39 | - Core/Size: Rename capWidth/capHeight to maxRenderWidth/maxRenderHeight 40 | - Core/Renderer: Only apply render width/height to canvas-based renderers 41 | - Core/Renderer: Call setRenderSize on renderer if present (MultiRenderer) 42 | - Core/Bind: Moved before renderer 43 | - Core/Fallback: Simply fallback message into wrapper + message 44 | 45 | ### 0.0.7 46 | 47 | - New Extra/Cursor: Set mouse cursor contextually. Can auto-hide cursor with time out. 48 | - New Core/Fallback: Abort and display standard message when WebGL is unavailable. 49 | - Initialize `three.Time.now` on install. 50 | 51 | ### 0.0.6 52 | 53 | - Core: 'ready' event now always fires on hot install 54 | - Core/Time: Frame count/time `frames` and variable speed `clock`/`step`. Use `speed` option to control. 55 | - Core/Fill: Configurable options for behavior, add `layout` option to position overlays like stats correctly. 56 | - Extra/Stats: Insert into containing DOM element. 57 | 58 | ### 0.0.5 59 | 60 | - Make canvas display as block in 'fill' 61 | 62 | ### 0.0.4 63 | 64 | - Shorthand syntax `foo:bar` for aliases in plugin list 65 | - Make api mechanism reusable as `THREE.Api` 66 | - Hot-swap plugins with `three.install/uninstall(name/plugins)` 67 | 68 | ### 0.0.3 69 | 70 | - this.api({...}, three) now auto-passes `three` as argument to API methods. 71 | - Move `bind` and `renderer` into their own plugins 72 | - Make bind mechanism reusable as `THREE.Binder` 73 | - Diff changes made with `$.api().set()`, provide `.changes` as well as all passed `.options` values 74 | - Ad-hoc aliases to override plug-ins on init 75 | 76 | ### 0.0.2 77 | 78 | - Declarative event binding 79 | - .on/.off/.trigger naming, with fallback for DOM/THREE objects 80 | - `controls` plug-in / examples 81 | -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [sritchie] 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | _Copyright (C) 2014+ Steven Wittens and contributors_ 2 | See `git blame` for details. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | the Software, and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # threestrap 2 | 3 | _Use Three.js with zero hassle._ 4 | 5 | Example 6 | 7 | --- 8 | 9 | Threestrap is a minimal, pluggable bootstrapper for Three.js that gets out of 10 | your way. 11 | 12 | While this is definitely a miniature framework, it's not really meant to wrap 13 | _your_ code, but rather the code you don't care about. 14 | 15 | Examples: 16 | 17 | - [Basic Cubes](http://acko.net/files/threestrap/basic_cube.html) 18 | - [Drag/Zoom Controls](http://acko.net/files/threestrap/controls_orbit_cube.html) 19 | - [WebGL + CSS 3D](http://acko.net/files/threestrap/multiple_renderers.html) 20 | - [VR + Controls](http://acko.net/files/threestrap/vr2.html) 21 | 22 | ## Usage 23 | 24 | Install via npm: 25 | 26 | ```sh 27 | npm install threestrap 28 | ``` 29 | 30 | Add `build/threestrap.js` to your Three.js: 31 | 32 | ```html 33 | 37 | 38 | 39 | ``` 40 | 41 | Get a threestrap context: 42 | 43 | ```javascript 44 | const three = new Threestrap.Bootstrap(); 45 | ``` 46 | 47 | This will create a full-page Three.js WebGL canvas, initialize the scene and 48 | camera, and set up a rendering loop. 49 | 50 | You can access the globals `three.scene` and `three.camera`, and bind events on 51 | the `three` context: 52 | 53 | ```javascript 54 | // Insert a cube 55 | const mesh = new THREE.Mesh( 56 | new THREE.CubeGeometry(0.5, 0.5, 0.5), 57 | new THREE.MeshNormalMaterial() 58 | ); 59 | three.scene.add(mesh); 60 | 61 | // Orbit the camera 62 | three.on("update", function () { 63 | var t = three.Time.now; 64 | three.camera.position.set(Math.cos(t), 0.5, Math.sin(t)); 65 | three.camera.lookAt(new THREE.Vector3()); 66 | }); 67 | ``` 68 | 69 | ## Configuration 70 | 71 | Threestrap is made out of plugins that each do one thing. The basic set up of 72 | `empty` or `core` gets you a fully-functional canvas in the body or a specific 73 | DOM element. 74 | 75 | Empty: 76 | 77 | - `fallback` - Displays a standard message with a link if WebGL is unavailable. 78 | - `bind` - Enables event/method binding. 79 | - `renderer` - Creates the `THREE.WebGLRenderer` (or a given class). 80 | - `size` - Autosizes canvas to fit or size to given dimensions. 81 | - `fill` - Removes margin/padding and sets positioning on the element. 82 | - `loop` - Runs the rendering loop. 83 | - `time` - Measures time and fps in seconds. Provides clocks. 84 | 85 | Core: 86 | 87 | - `scene` - Creates the `THREE.Scene` 88 | - `camera` - Creates the `THREE.Camera` 89 | - `render` - Renders the global scene and camera directly. 90 | - `warmup` - Hide canvas for first few frames to avoid stuttering. 91 | 92 | Additional plug-ins can be added, or the default set can be overridden on a case 93 | by case basis. `empty` is a do-it-yourself set up. 94 | 95 | Shorthands: 96 | 97 | ```javascript 98 | // Core only 99 | var three = new Threestrap.Bootstrap(); 100 | 101 | // Pass in list of plugins 102 | var three = new Threestrap.Bootstrap("core", "stats"); 103 | var three = new Threestrap.Bootstrap(["core", "stats"]); 104 | 105 | // Insert into specific element 106 | var three = new Threestrap.Bootstrap(element); 107 | var three = new Threestrap.Bootstrap(element, "core", "stats"); 108 | var three = new Threestrap.Bootstrap(element, ["core", "stats"]); 109 | 110 | // Replace plugins ad-hoc 111 | var three = new Threestrap.Bootstrap(["core", "stats", "render:myRender"]); 112 | ``` 113 | 114 | The following global options are available with these defaults: 115 | 116 | ```javascript 117 | var three = new Threestrap.Bootstrap({ 118 | init: true, // Initialize on creation 119 | 120 | element: document.body, // Containing element 121 | 122 | plugins: [ 123 | // Active plugins 124 | "core", // Use all core plugins 125 | // 'render:myRender' // Ad-hoc overrides 126 | ], 127 | 128 | aliases: { 129 | // Ad-hoc overrides 130 | // 'render': 'myRender', 131 | // 'alias': ['myFoo', 'myBar'] 132 | }, 133 | }); 134 | ``` 135 | 136 | When `init` is set to false, initialization only happens when manually calling 137 | `three.init()`. To destroy the widget, call `three.destroy()`. 138 | 139 | Plugins can make objects and methods available on the threestrap context, like 140 | `three.Time.now` or `three.Loop.start()`. 141 | 142 | 143 | ## Plugins 144 | 145 | To enable a plug-in, include its name in the `plugins` field. Plugins are 146 | installed in the given order. 147 | 148 | Plug-in specific options are grouped under the plug-in's name: 149 | 150 | ```javascript 151 | const three = new Threestrap.Bootstrap({ 152 | plugins: ["core", "stats"], 153 | size: { 154 | width: 1280, 155 | height: 720, 156 | }, 157 | camera: { 158 | fov: 40, 159 | }, 160 | }); 161 | ``` 162 | 163 | The following aliases are available: 164 | 165 | - `empty` = `fallback`, `bind`, `renderer`, `size`, `fill`, `loop`, `time` 166 | - `core` = `empty` + `scene`, `camera`, `render`, `warmup` 167 | 168 | ## Events 169 | 170 | Threestrap plugins broadcast events to each other, like `resize` or `render`. 171 | 172 | You can listen for events with `.on()` and unset them with `.off()`. 173 | 174 | ```javascript 175 | three.on("event", function (event, three) {}); 176 | ``` 177 | 178 | ```javascript 179 | var handler = function () {}; 180 | three.on("event", handler); 181 | three.off("event", handler); 182 | ``` 183 | 184 | You can also bind events directly to object methods using `.bind`: 185 | 186 | ```javascript 187 | const object = { 188 | render: function (event, three) {}, 189 | yup: function (event, three) {}, 190 | redraw: function (event, three) {}, 191 | resize: function (event, three) {}, 192 | change: function (event, three) {}, 193 | }; 194 | 195 | // Bind three.render event to object.render(event, three) 196 | three.bind("render", object); 197 | 198 | // Bind three.ready event to object.yup(event, three); 199 | three.bind("ready:yup", object); 200 | 201 | // Bind object.change event to object.redraw(event, three); 202 | three.bind("this.change:redraw", object); 203 | 204 | // Bind window.resize event to object.resize(event, three); 205 | three.bind("window.resize", object); 206 | 207 | // Bind DOM element's onchange event to object.change(event, three); 208 | three.bind([element, "change"], object); 209 | ``` 210 | 211 | ## Docs 212 | 213 | - [Core reference](https://github.com/unconed/threestrap/blob/master/docs/core.md) 214 | - [Extra plugins reference](https://github.com/unconed/threestrap/blob/master/docs/extra.md) 215 | - [Write custom plugins](https://github.com/unconed/threestrap/blob/master/docs/custom.md) 216 | 217 | --- 218 | 219 | Steven Wittens - http://acko.net/ 220 | -------------------------------------------------------------------------------- /docs/core.md: -------------------------------------------------------------------------------- 1 | Threestrap - Core Reference 2 | === 3 | 4 | * API 5 | 6 | ```javascript 7 | Threestrap.Bootstrap() // Create bootstrap context 8 | 9 | Threestrap.Bootstrap('plugin', ...) // With given plugins 10 | Threestrap.Bootstrap(['plugin', ...]) 11 | Threestrap.Bootstrap(['plugin', "plugin:plugin"]) // and ad-hoc overrides 12 | 13 | Threestrap.Bootstrap(element, "plugin", ...) // Inside given DOM element 14 | Threestrap.Bootstrap(element, ["plugin", ...]) // 15 | 16 | Threestrap.Bootstrap({ // With given options 17 | init: true, // Initialize on creation 18 | 19 | element: document.body, // Containing element 20 | 21 | plugins: [ // Active plugins 22 | 'core', // Use all core plugins 23 | ], 24 | 25 | aliases: { // Ad-hoc overrides 26 | // 'alias': 'plugin', 27 | // 'alias': ['plugin', ...], 28 | } 29 | 30 | 31 | three.init() // Initialize threestrap instance 32 | three.destroy() // Destroy threestrap instance 33 | 34 | three.install('plugin', ...) // Install plugin(s) on the fly 35 | three.install(['plugin', ...]) 36 | three.uninstall('plugin', ...) // Uninstall plugin(s) on the fly 37 | three.uninstall(['plugin', ...]) 38 | ``` 39 | 40 | * Properties 41 | 42 | ```javascript 43 | three.element; // Containing element 44 | three.plugins; // Collection of installed plugins by name 45 | ```` 46 | 47 | * Event listeners 48 | 49 | ```javascript 50 | // Listen for three.event events 51 | three.on('event', function (event, three) { }); 52 | ``` 53 | ```javascript 54 | // Remove event listener 55 | three.off('event', handler); 56 | ``` 57 | 58 | ```javascript 59 | // Trigger a threestrap event. 60 | three.trigger({ 61 | type: 'event', 62 | // ... 63 | }); 64 | ``` 65 | 66 | * Events 67 | 68 | ```javascript 69 | // Fires once after all plugins have been installed 70 | three.on('ready', function (event, three) { }); 71 | ``` 72 | 73 | renderer 74 | --- 75 | Creates the Three.js renderer of the given class. 76 | 77 | * Options 78 | 79 | ```javascript 80 | { 81 | klass: THREE.WebGLRenderer, // Renderer class 82 | parameters: { // Parameters passed to Three.js renderer 83 | depth: true, 84 | stencil: true, 85 | preserveDrawingBuffer: true, 86 | antialias: true, 87 | }, 88 | } 89 | ``` 90 | 91 | * Properties 92 | 93 | ```javascript 94 | three.canvas; // Canvas / DOM element 95 | three.renderer; // Three renderer 96 | ``` 97 | 98 | bind 99 | --- 100 | Enables event/method binding on context and installed plug-ins (nothing works without it). 101 | 102 | * API 103 | 104 | ```javascript 105 | // Bind threestrap 'event' events to object.event(event, three) 106 | three.bind('event', object); 107 | ``` 108 | 109 | ```javascript 110 | // Bind threestrap 'event' events to object.method(event, three) 111 | three.bind('event:method', object); 112 | ``` 113 | 114 | ```javascript 115 | // Bind target's 'event' events to object.method(event, three) 116 | // where target is one of: 117 | // - three: threestrap context 118 | // - this: the listening object itself 119 | // - element: the containing element 120 | // - canvas: the canvas 121 | // - window: window object 122 | three.bind('target.event:method', object); 123 | ``` 124 | 125 | ```javascript 126 | // Bind target's 'event' events to object.method(event, three) 127 | // where target is any object with on / off / addEventListener / removeEventListener methods. 128 | three.bind([ target, 'event:method' ], object); 129 | ``` 130 | 131 | ```javascript 132 | // Unbind all bound methods 133 | three.unbind(object); 134 | ``` 135 | 136 | size 137 | --- 138 | Autosizes canvas to fill its container or size to given dimensions. Force aspect ratio, limit maximum frame buffer size and apply a global scale. 139 | 140 | * Options 141 | 142 | ```javascript 143 | { 144 | width: null, // Fixed width in pixels 145 | height: null, // Fixed height in pixels 146 | aspect: null, // Fixed aspect ratio, e.g. 16/9 147 | scale: 1, // Scale factor. e.g. scale 1/2 renders at half resolution. 148 | maxRenderWidth: Infinity, // Maximum width in pixels of framebuffer 149 | maxRenderHeight: Infinity, // Maximum height in pixels of framebuffer 150 | devixePixelRatio: true, // Whether to automatically adjust for high DPI displays 151 | } 152 | ``` 153 | 154 | * API 155 | 156 | ```javascript 157 | // Methods 158 | three.Size.set({ }); // Set options 159 | three.Size.get(); // Get options 160 | ``` 161 | 162 | * Properties 163 | 164 | ```javascript 165 | three.Size.renderWidth; // Width of frame buffer 166 | three.Size.renderHeight; // Height of frame buffer 167 | three.Size.viewWidth; // Width of canvas on page 168 | three.Size.viewHeight; // Height of canvas on page 169 | three.Size.aspect; // Aspect ratio of view 170 | three.Size.pixelRatio; // Pixel ratio (render height / view height) 171 | ``` 172 | 173 | * Events 174 | 175 | ```javascript 176 | // Canvas was resized to new dimensions. 177 | three.on('resize', function (event, three) { 178 | // event == 179 | { 180 | renderWidth: 100, 181 | renderHeight: 100, 182 | viewWidth: 100, 183 | viewHeight: 100, 184 | aspect: 1, 185 | } 186 | } 187 | ``` 188 | 189 | fill 190 | --- 191 | Makes sure canvas can fill the entire window when directly inside the body. Helps positioning when inside a DOM element. 192 | 193 | * Options 194 | ```javascript 195 | { 196 | block: true, // Make canvas a block element 197 | body: true, // Set auto height/margin/padding on 198 | layout: true, // Set position relative on container if needed to ensure layout 199 | } 200 | ``` 201 | 202 | loop 203 | --- 204 | Runs the rendering loop and asks plugins to update or render themselves. 205 | 206 | * Options 207 | 208 | ```javascript 209 | { 210 | start: true, // Begin immediately on ready 211 | each: 1, // Render only every n'th frame 212 | } 213 | ``` 214 | 215 | * API 216 | 217 | ```javascript 218 | three.Loop.start(); // Start loop 219 | three.Loop.stop(); // Stop loop 220 | ``` 221 | 222 | * Properties 223 | 224 | ```javascript 225 | three.Loop.running; // Is loop running? 226 | ``` 227 | 228 | * Events 229 | 230 | ```javascript 231 | // Loop has been started 232 | three.on('start', function () { }); 233 | ``` 234 | 235 | ```javascript 236 | // Loop has been stopped 237 | three.on('stop', function () { }); 238 | ``` 239 | 240 | ```javascript 241 | // Prepare for rendering 242 | three.on('pre', function () { }); 243 | ``` 244 | 245 | ```javascript 246 | // Update state of objects 247 | three.on('update', function () { }); 248 | ``` 249 | 250 | ```javascript 251 | // Render objects 252 | three.on('render', function () { }); 253 | ``` 254 | 255 | ```javascript 256 | // Finish up after rendering 257 | three.on('post', function () { }); 258 | ``` 259 | 260 | time 261 | --- 262 | Measures time and fps in seconds. 263 | 264 | * Options 265 | 266 | ```javascript 267 | { 268 | speed: 1, // Clock speed (2 = fast forward, 0.5 = slow motion) 269 | warmup: 0, // Wait N frames before starting clock 270 | timeout: 1 // Ignore ticks longer than this, effectively pausing the clock 271 | } 272 | ``` 273 | 274 | 275 | * Properties 276 | 277 | ```javascript 278 | three.Time.now // Time since 1970 (seconds) 279 | 280 | three.Time.clock // Clock (seconds since start) 281 | three.Time.step // Clock step (seconds) 282 | 283 | three.Time.frames // Frame count 284 | three.Time.time // Real time (seconds since start) 285 | three.Time.delta // Last frame time (seconds) 286 | 287 | three.Time.average // Average frame time (seconds) 288 | three.Time.fps // Average frames per second 289 | ``` 290 | 291 | scene 292 | --- 293 | Makes a scene available. 294 | 295 | * Properties 296 | 297 | ```javascript 298 | three.scene // Global scene 299 | ``` 300 | 301 | camera 302 | --- 303 | Makes a camera available. 304 | 305 | * Options 306 | 307 | ```javascript 308 | { 309 | near: .1, // Near clip plane 310 | far: 10000, // Far clip plane 311 | 312 | type: 'perspective', // Perspective camera 313 | fov: 60, // Field of view 314 | aspect: 'auto', // Aspect ratio (number or 'auto') 315 | 316 | // type: 'orthographic', // Orthographic camera 317 | left: -1, // Bounding box 318 | right: 1, 319 | bottom: -1, 320 | top: 1, 321 | 322 | klass: null, // Custom class/parameters 323 | parameters: null, // if you really want to 324 | } 325 | ``` 326 | 327 | * API 328 | 329 | ```javascript 330 | three.Camera.set({ }); // Set options 331 | three.Camera.get(); // Get options 332 | ``` 333 | 334 | * Properties 335 | 336 | ```javascript 337 | three.camera; // Global camera 338 | ``` 339 | 340 | * Events 341 | 342 | ```javascript 343 | // Camera was recreated / changed 344 | three.on('camera', function (event, three) { 345 | // event.camera 346 | } 347 | ``` 348 | 349 | render 350 | --- 351 | Renders the global scene and camera directly. 352 | 353 | * No options or API 354 | 355 | 356 | fallback 357 | --- 358 | Displays a standard message with a link if WebGL is unavailable. 359 | 360 | * Options 361 | 362 | ```javascript 363 | { 364 | force: false, // Force fallback (for testing) 365 | fill: true, // Use 'fill' plugin when displaying message 366 | 367 | // Message wrapper (center horizontally/vertically) 368 | begin: '
'+ 370 | '
', 371 | end: '
', 372 | 373 | // Message 374 | message: 'This example requires WebGL
'+ 375 | 'Visit get.webgl.org for more info', 376 | } 377 | ``` 378 | 379 | * Properties 380 | 381 | ```javascript 382 | three.fallback; // True if fallback was triggered. 383 | ``` 384 | 385 | warmup 386 | --- 387 | Hide canvas for first few frames to avoid stuttering while JS/GL warms up. 388 | 389 | * Options 390 | 391 | ```javascript 392 | { 393 | delay: 2, // Number of frames to wait before showing canvas 394 | } 395 | -------------------------------------------------------------------------------- /docs/custom.md: -------------------------------------------------------------------------------- 1 | Threestrap - Custom Plugins 2 | === 3 | 4 | See below for scaffold. Use `Bootstrap.registerPlugin` to make a new plug-in 5 | available, passing in a prototype for the class. 6 | 7 | Init 8 | --- 9 | 10 | * `.install(three)` and `.uninstall(three)` are for initialization and cleanup respectively 11 | * plugins should install themselves into `three`, preferably in a namespaced object 12 | 13 | Recommended format is `three.FooBar.…` for a *Foo Bar* plugin's namespace. Creating a `three.fooBar` global is allowed for singletons or other well known objects. Other named globals are discouraged, try to keep the `three` namespace clean. 14 | 15 | Config 16 | --- 17 | 18 | * `this.options` contains the plugin's options, defaults are set in `.defaults` 19 | * use `this.api()` to create set/get helpers for changing options 20 | * when `api.set({...})` is called, the `change` event fires on `this`. `event.changes` lists the values that actually changed 21 | * additional API methods can be added, which receive `three` as their final argument 22 | 23 | Events 24 | --- 25 | 26 | * `.listen` declares a list of event/method bindings as an array 27 | * use `three.on()/.off()/.bind()` for manual binding 28 | * method bindings are automatically unbound when the plugin is uninstalled 29 | 30 | Examples 31 | --- 32 | 33 | See `src/extra/` for example plug-ins. 34 | 35 | Scaffold 36 | --- 37 | 38 | ```javascript 39 | Threestrap.Bootstrap.registerPlugin('magic', { 40 | 41 | // Configuration defaults 42 | defaults: { 43 | foo: 'bar', 44 | }, 45 | 46 | // Declare event listeners for plugin methods 47 | // 48 | // format: "object.event:method" 49 | // allowed objects: this, three, element, canvas, window 50 | // default: "three.event:event" 51 | // 52 | // alt format: [object, "event:method"] 53 | listen: ['this.change', 'ready:yup', [ document.body, 'click' ]], 54 | 55 | // Initialize resources, bind events 56 | install: function (three) { 57 | 58 | // Listen manually for outside events 59 | // three.on(...); 60 | 61 | // Make a public API (includes .set() / .get()) 62 | // Calling `….api({...}, three)` will pass `three` as the final argument to all API methods. 63 | three.Magic = this.api({ 64 | 65 | // three.Magic.ping() 66 | ping: function (three) { 67 | // Trigger own events 68 | three.trigger({ type: 'magic', ping: true }); 69 | }.bind(this), 70 | 71 | }, 72 | three); 73 | 74 | // Expose values globally (discouraged) 75 | three.magic = 1; 76 | three.doMagic = three.Magic.ping.bind(this); 77 | }, 78 | 79 | // Destroy resources, unbind events 80 | uninstall: function (three) { 81 | 82 | // Remove manual event listeners 83 | // three.off(...); 84 | 85 | // Remove from context 86 | delete three.Magic; 87 | delete three.magic; 88 | delete three.doMagic; 89 | }, 90 | 91 | // body.click event handler 92 | click: function (event, three) { 93 | }, 94 | 95 | // this.change event handler 96 | change: function (event, three) { 97 | // event.type == 'change' 98 | // event.changes == {...} 99 | // this.options reflects the new state, i.e.: 100 | // this.options.foo == 'bar' 101 | }, 102 | 103 | // three.ready event handler 104 | yup: function (event, three) { 105 | // event.type == 'ready' 106 | }, 107 | 108 | }); 109 | ``` 110 | 111 | Call `Threestrap.Bootstrap.unregisterPlugin('plugin')` to remove. 112 | 113 | 114 | Aliases 115 | --- 116 | 117 | Make an alias for a set of plugins, like so: 118 | 119 | ``` 120 | Threestrap.Bootstrap.registerAlias('empty', ['size', 'fill', 'loop', 'time']); 121 | ``` 122 | 123 | Call `Threestrap.Bootstrap.unregisterAlias('alias')` to remove. 124 | -------------------------------------------------------------------------------- /docs/extra.md: -------------------------------------------------------------------------------- 1 | Threestrap - Extra Plugins 2 | === 3 | 4 | vr 5 | --- 6 | Supports rendering to HMDs like the Oculus Rift or Google Cardboard. 7 | 8 | * Options: 9 | 10 | ```javascript 11 | { 12 | mode: 'auto', // Set '2d' to force VR off 13 | device: null, // Force a specific device ID 14 | fov: 80, // Set emulated FOV for fallback / cardboard use 15 | } 16 | ``` 17 | 18 | * API 19 | 20 | ```javascript 21 | three.VR.set({ }); // Set options 22 | three.VR.get(); // Get options 23 | ``` 24 | 25 | * Properties 26 | 27 | ```javascript 28 | three.VR.active // Whether stereoscopic VR is currently active 29 | three.VR.devices // List of available VR devices 30 | three.VR.hmd // Current head-mounted display device 31 | three.VR.sensor // Current positional sensor device 32 | three.VR.renderer // VRRenderer instance 33 | three.VR.state // Last sensor state 34 | ``` 35 | 36 | * Events 37 | 38 | ```javascript 39 | // VR mode was activated / deactivated 40 | three.on('vr', function (event, three) { 41 | // event == 42 | { 43 | active: true, 44 | //active: false, 45 | hmd: ... 46 | sensor: ... 47 | } 48 | } 49 | ``` 50 | 51 | fullscreen 52 | --- 53 | Supports going fullscreen via an API or a keypress. Integrates with `vr` if present. 54 | 55 | * Options: 56 | 57 | ```javascript 58 | { 59 | key: 'f', // Keyboard letter to toggle fullscreen mode with (e.g. 'f') or 'null' to disable 60 | } 61 | ``` 62 | 63 | * API 64 | 65 | ```javascript 66 | three.Fullscreen.set({ }); // Set options 67 | three.Fullscreen.get(); // Get options 68 | three.Fullscreen.toggle(); // Go fullscreen / exit fullscreen 69 | ``` 70 | 71 | * Properties 72 | 73 | ```javascript 74 | three.Fullscreen.active // Whether fullscreen is currently active 75 | ``` 76 | 77 | * Events 78 | 79 | ```javascript 80 | // Fullscreen mode was activated / deactivated 81 | three.on('fullscreen', function (event, three) { 82 | // event == 83 | { 84 | active: true, 85 | //active: false, 86 | } 87 | } 88 | ``` 89 | 90 | ui 91 | --- 92 | Minimal UI for fullscreen / VR mode. 93 | 94 | * Options: 95 | ```javascript 96 | { 97 | // Class (white / black) 98 | theme: 'white', 99 | // Injected CSS. 100 | style: '.threestrap-ui { position: absolute; bottom: 5px; right: 5px; float: left; }'+ 101 | '.threestrap-ui button { border: 0; background: none;'+ 102 | ' vertical-align: middle; font-weight: bold; } '+ 103 | '.threestrap-ui .glyphicon { top: 2px; font-weight: bold; } '+ 104 | '@media (max-width: 640px) { .threestrap-ui button { font-size: 120% } }'+ 105 | '.threestrap-white button { color: #fff; text-shadow: 0 1px 1px rgba(0, 0, 0, 1), '+ 106 | '0 1px 3px rgba(0, 0, 0, 1); }'+ 107 | '.threestrap-black button { color: #000; text-shadow: 0 0px 1px rgba(255, 255, 255, 1), '+ 108 | '0 0px 2px rgba(255, 255, 255, 1), '+ 109 | '0 0px 2px rgba(255, 255, 255, 1) }' 110 | } 111 | ``` 112 | 113 | stats 114 | --- 115 | Shows live FPS stats in the corner (with Stats.js) 116 | 117 | * Properties 118 | 119 | ```javascript 120 | three.stats; // Stats() object 121 | ``` 122 | 123 | controls 124 | --- 125 | Binds a THREE camera controller to the global camera. Note: you must manually include the .js controller. See `vendor/controls/` and three.js' own examples. 126 | 127 | * Options: 128 | 129 | ```javascript 130 | { 131 | klass: THREE.OrbitControls, // Control class 132 | parameters: { // Parameters for class 133 | }, 134 | } 135 | ``` 136 | 137 | * API 138 | 139 | ```javascript 140 | three.Controls.set({ }); // Set options 141 | three.Controls.get(); // Get options 142 | ``` 143 | 144 | * Properties 145 | 146 | ```javascript 147 | three.controls; // Global camera controls 148 | ``` 149 | 150 | cursor 151 | --- 152 | Sets the mouse cursor contextually. If controls are present, `move` is used, otherwise a default. 153 | 154 | * Options: 155 | 156 | ```javascript 157 | { 158 | cursor: null, // Force a specific CSS cursor (e.g. 'pointer') 159 | hide: false, // Auto-hide the cursor after inactivity 160 | timeout: 3, // Time out for hiding (seconds) 161 | } 162 | ``` 163 | 164 | -------------------------------------------------------------------------------- /examples/basic_cube.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Threestrap - Basic Cubes 6 | 10 | 14 | 15 | 16 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /examples/basic_cube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconed/threestrap/c28ea724be91228ea686a8e85ccf680307c03c65/examples/basic_cube.png -------------------------------------------------------------------------------- /examples/controls_firstperson_cube.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Threestrap - Camera First Person Controls 6 | 10 | 14 | 18 | 19 | 20 |
21 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /examples/controls_orbit_cube.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Threestrap - Camera Orbit Controls 6 | 10 | 14 | 18 | 19 | 20 | 26 |

Drag/Zoom Controls

27 |
28 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /examples/element_cube.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Threestrap - Basic Cubes in DOM element 6 | 10 | 14 | 15 | 16 | 22 |

Cubes in a Div

23 |
24 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /examples/empty.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Threestrap - Empty 6 | 10 | 14 | 15 | 16 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/multiple_renderers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Threestrap - Multiple Renderers 6 | 10 | 14 | 18 | 19 | 20 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /examples/multiple_threestraps.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Threestrap - Multiple Threestraps 6 | 10 | 14 | 15 | 16 | 25 |

Two WebGL Renderers

26 |
27 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /examples/tooltip.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Threestrap - Tooltip 6 | 10 | 14 | 18 | 19 | 20 | 31 |
32 |
Tooltip!
33 |
34 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /examples/vr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Threestrap - VR 6 | 10 | 14 | 18 | 19 | 20 | 21 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /examples/vr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconed/threestrap/c28ea724be91228ea686a8e85ccf680307c03c65/examples/vr.png -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require("gulp"); 2 | const karma = require("karma"); 3 | 4 | const parseConfig = karma.config.parseConfig; 5 | const KarmaServer = karma.Server; 6 | 7 | const source = ["src/**/*.js"]; 8 | 9 | const test = source.concat(["test/**/*.spec.js"]); 10 | 11 | gulp.task("karma", function (done) { 12 | parseConfig( 13 | __dirname + "/karma.conf.js", 14 | { files: test, singleRun: true }, 15 | { promiseConfig: true, throwErrors: true } 16 | ).then( 17 | (karmaConfig) => { 18 | new KarmaServer(karmaConfig, done).start(); 19 | done(); 20 | }, 21 | (_rejectReason) => {} 22 | ); 23 | }); 24 | 25 | gulp.task("watch-karma", function () { 26 | return gulp.src(test).pipe( 27 | karma({ 28 | configFile: "karma.conf.js", 29 | action: "watch", 30 | }) 31 | ); 32 | }); 33 | -------------------------------------------------------------------------------- /karma.conf.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "karma" 2 | // Karma configuration 3 | // Generated on Wed Jan 22 2014 23:58:15 GMT-0800 (PST) 4 | 5 | const config = (config: Config) => { 6 | config.set({ 7 | // base path, that will be used to resolve files and exclude 8 | basePath: "", 9 | 10 | // frameworks to use 11 | frameworks: ["jasmine"], 12 | 13 | files: [ 14 | "build_tests/tests.js" 15 | ], 16 | 17 | // list of files to exclude 18 | exclude: [], 19 | 20 | // test results reporter to use 21 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 22 | reporters: ["progress"], 23 | 24 | // web server port 25 | port: 9876, 26 | 27 | // enable / disable colors in the output (reporters and logs) 28 | colors: true, 29 | 30 | // level of logging 31 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 32 | logLevel: config.LOG_INFO, 33 | 34 | // enable / disable watching file and executing tests whenever any file changes 35 | autoWatch: true, 36 | 37 | // Start these browsers, currently available: 38 | // - Chrome 39 | // - ChromeCanary 40 | // - Firefox 41 | // - Opera (has to be installed with `npm install karma-opera-launcher`) 42 | // - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`) 43 | // - PhantomJS 44 | // - IE (only Windows; has to be installed with `npm install karma-ie-launcher`) 45 | browsers: ["Chrome"], 46 | 47 | // If browser does not capture in given timeout [ms], kill it 48 | captureTimeout: 60000, 49 | }); 50 | }; 51 | 52 | export default config -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "threestrap", 3 | "version": "0.5.1", 4 | "description": "Minimal Three.js Bootstrapper", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/unconed/threestrap.git" 8 | }, 9 | "main": "src/index.js", 10 | "dependencies": { 11 | "stats.js": "^0.17.0" 12 | }, 13 | "files": [ 14 | "/src", 15 | "build", 16 | "*.md", 17 | "FUNDING.yml" 18 | ], 19 | "peerDependencies": { 20 | "three": ">=0.118.0" 21 | }, 22 | "scripts": { 23 | "build": "webpack --mode=production --config-name=threestrap", 24 | "dev": "webpack --mode=development --watch --config-name=threestrap", 25 | "prepack": "npm run build", 26 | "lint": "eslint src/**/*.js", 27 | "test": "webpack --config-name=tests && karma start --single-run --browsers=ChromeHeadless", 28 | "test:watch": "concurrently --names=webpack,karma -c=magenta,cyan 'webpack --watch --config-name=tests' 'karma start'" 29 | }, 30 | "prettier": {}, 31 | "devDependencies": { 32 | "@types/glob": "^8.0.0", 33 | "@types/karma": "^6.3.3", 34 | "@types/node": "^18.11.17", 35 | "concurrently": "^7.6.0", 36 | "eslint": "^7.28.0", 37 | "eslint-plugin-jasmine": "^4.1.2", 38 | "glob": "^8.0.3", 39 | "gulp": "^4.0.2", 40 | "jasmine-core": "^4.5.0", 41 | "karma": "^6.3.3", 42 | "karma-chrome-launcher": "^3.1.1", 43 | "karma-jasmine": "^4.0.1", 44 | "prettier": "2.3.1", 45 | "terser-webpack-plugin": "^5.3.6", 46 | "ts-node": "^10.9.1", 47 | "typescript": "^4.9.4", 48 | "webpack": "^5.75.0", 49 | "webpack-cli": "^5.0.1", 50 | "webpack-stream": "^6.1.2" 51 | }, 52 | "author": "Steven Wittens", 53 | "license": "MIT" 54 | } 55 | -------------------------------------------------------------------------------- /src/aliases.js: -------------------------------------------------------------------------------- 1 | import { Bootstrap } from "./bootstrap"; 2 | 3 | Bootstrap.registerAlias("empty", [ 4 | "fallback", 5 | "bind", 6 | "renderer", 7 | "size", 8 | "fill", 9 | "loop", 10 | "time", 11 | ]); 12 | 13 | Bootstrap.registerAlias("core", [ 14 | "empty", 15 | "scene", 16 | "camera", 17 | "render", 18 | "warmup", 19 | ]); 20 | 21 | Bootstrap.registerAlias("VR", ["core", "cursor", "fullscreen", "render:vr"]); 22 | -------------------------------------------------------------------------------- /src/api.js: -------------------------------------------------------------------------------- 1 | export class Api { 2 | static apply(object) { 3 | object.set = function (options) { 4 | const o = this.options || {}; 5 | 6 | // Diff out changes 7 | const changes = Object.entries(options).reduce(function ( 8 | result, 9 | [key, value] 10 | ) { 11 | if (o[key] !== value) result[key] = value; 12 | return result; 13 | }, 14 | {}); 15 | 16 | this.options = Object.assign(o, changes); 17 | 18 | // Notify 19 | this.trigger({ type: "change", options: options, changes: changes }); 20 | }; 21 | 22 | object.get = function () { 23 | return this.options; 24 | }; 25 | 26 | object.api = function (object, context) { 27 | if (!object) { 28 | object = {}; 29 | } 30 | 31 | // Append context argument to API methods 32 | context && 33 | Object.entries(object).forEach(function ([key, callback]) { 34 | if (typeof callback === "function") { 35 | object[key] = (...args) => callback(...args, context); 36 | } 37 | }); 38 | 39 | object.set = this.set.bind(this); 40 | object.get = this.get.bind(this); 41 | 42 | return object; 43 | }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/binder.js: -------------------------------------------------------------------------------- 1 | import { EventDispatcher } from "three"; 2 | 3 | export class Binder { 4 | static bind(context, globals) { 5 | return function (key, object) { 6 | // Prepare object 7 | if (!object.__binds) { 8 | object.__binds = []; 9 | } 10 | 11 | // Set base target 12 | let fallback = context; 13 | 14 | if (Array.isArray(key)) { 15 | fallback = key[0]; 16 | key = key[1]; 17 | } 18 | 19 | // Match key 20 | const match = /^([^.:]*(?:\.[^.:]+)*)?(?::(.*))?$/.exec(key); 21 | const path = match[1].split(/\./g); 22 | 23 | const name = path.pop(); 24 | const dest = match[2] || name; 25 | 26 | // Whitelisted objects 27 | const selector = path.shift(); 28 | 29 | let target = 30 | { 31 | this: object, 32 | }[selector] || 33 | globals[selector] || 34 | context[selector] || 35 | fallback; 36 | 37 | // Look up keys 38 | while (target && (key = path.shift())) { 39 | target = target[key]; 40 | } 41 | 42 | // Attach event handler at last level 43 | if (target && (target.on || target.addEventListener)) { 44 | const callback = function (event) { 45 | object[dest] && object[dest](event, context); 46 | }; 47 | 48 | // Polyfill for both styles of event listener adders 49 | Binder._polyfill(target, ["addEventListener", "on"], function (method) { 50 | target[method](name, callback); 51 | }); 52 | 53 | // Store bind for removal later 54 | const bind = { target: target, name: name, callback: callback }; 55 | object.__binds.push(bind); 56 | 57 | // Return callback 58 | return callback; 59 | } else { 60 | throw "Cannot bind '" + key + "' in " + this.__name; 61 | } 62 | }; 63 | } 64 | 65 | static unbind() { 66 | return function (object) { 67 | // Remove all binds belonging to object 68 | if (object.__binds) { 69 | object.__binds.forEach( 70 | function (bind) { 71 | // Polyfill for both styles of event listener removers 72 | Binder._polyfill( 73 | bind.target, 74 | ["removeEventListener", "off"], 75 | function (method) { 76 | bind.target[method](bind.name, bind.callback); 77 | } 78 | ); 79 | }.bind(this) 80 | ); 81 | 82 | object.__binds = []; 83 | } 84 | }; 85 | } 86 | 87 | static apply(object) { 88 | object.trigger = Binder._trigger; 89 | object.triggerOnce = Binder._triggerOnce; 90 | 91 | object.hasEventListener = EventDispatcher.prototype.hasEventListener; 92 | object.addEventListener = EventDispatcher.prototype.addEventListener; 93 | object.removeEventListener = EventDispatcher.prototype.removeEventListener; 94 | 95 | object.on = object.addEventListener; 96 | object.off = object.removeEventListener; 97 | object.dispatchEvent = object.trigger; 98 | } 99 | 100 | static _triggerOnce(event) { 101 | this.trigger(event); 102 | if (this._listeners) { 103 | delete this._listeners[event.type]; 104 | } 105 | } 106 | 107 | static _trigger(event) { 108 | if (this._listeners === undefined) return; 109 | 110 | const type = event.type; 111 | let listeners = this._listeners[type]; 112 | if (listeners !== undefined) { 113 | listeners = listeners.slice(); 114 | const length = listeners.length; 115 | 116 | event.target = this; 117 | for (let i = 0; i < length; i++) { 118 | // add original target as parameter for convenience 119 | listeners[i].call(this, event, this); 120 | } 121 | } 122 | } 123 | 124 | static _polyfill(object, methods, callback) { 125 | methods.map(function (_method) { 126 | return object.method; 127 | }); 128 | if (methods.length) callback(methods[0]); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/bootstrap.js: -------------------------------------------------------------------------------- 1 | import { Api } from "./api"; 2 | import { Binder } from "./binder"; 3 | 4 | function isString(str) { 5 | return str && typeof str.valueOf() === "string"; 6 | } 7 | 8 | /** 9 | * Like Array.prototype.forEach, but allows the callback to return false to 10 | * abort the loop 11 | */ 12 | const each = (array, cb) => { 13 | let i = 0; 14 | for (const item of array) { 15 | const success = cb(item, i, array); 16 | if (success === false) break; 17 | i++ 18 | } 19 | } 20 | 21 | export class Bootstrap { 22 | static initClass() { 23 | this.Plugins = {}; 24 | this.Aliases = {}; 25 | } 26 | 27 | static registerPlugin(name, spec) { 28 | const ctor = function (options) { 29 | Bootstrap.Plugin.call(this, options); 30 | this.__name = name; 31 | }; 32 | ctor.prototype = Object.assign(new Bootstrap.Plugin(), spec); 33 | 34 | this.Plugins[name] = ctor; 35 | } 36 | 37 | static unregisterPlugin(name) { 38 | delete this.Plugins[name]; 39 | } 40 | 41 | static registerAlias(name, plugins) { 42 | this.Aliases[name] = plugins; 43 | } 44 | 45 | static unregisterAlias(name) { 46 | delete this.Aliases[name]; 47 | } 48 | 49 | constructor(options) { 50 | if (options) { 51 | let args = [].slice.apply(arguments); 52 | options = {}; 53 | 54 | // (element, ...) 55 | if (args[0] instanceof Node) { 56 | const node = args[0]; 57 | args = args.slice(1); 58 | options.element = node; 59 | } 60 | 61 | // (..., plugin, plugin, plugin) 62 | if (isString(args[0])) { 63 | options.plugins = args; 64 | } else if (Array.isArray(args[0])) { 65 | // (..., [plugin, plugin, plugin]) 66 | options.plugins = args[0]; 67 | } else if (args[0]) { 68 | // (..., options) 69 | 70 | // else, merge any arguments on the right that have NOT been set into the 71 | // options dict on the left. 72 | options = Object.assign({}, args[0], options); 73 | } 74 | } 75 | 76 | // Apply defaults 77 | const defaultOpts = { 78 | init: true, 79 | element: document.body, 80 | plugins: ["core"], 81 | aliases: {}, 82 | plugindb: Bootstrap.Plugins || {}, 83 | aliasdb: Bootstrap.Aliases || {}, 84 | }; 85 | 86 | this.__options = Object.assign({}, defaultOpts, options || {}); 87 | 88 | // Hidden state 89 | this.__inited = false; 90 | this.__destroyed = false; 91 | this.__installed = []; 92 | 93 | // Query element 94 | let element = this.__options.element; 95 | if (element === "" + element) { 96 | element = document.querySelector(element); 97 | } 98 | 99 | // Global context 100 | this.plugins = {}; 101 | this.element = element; 102 | 103 | // Update cycle 104 | this.trigger = this.trigger.bind(this); 105 | this.frame = this.frame.bind(this); 106 | this.events = ["pre", "update", "render", "post"].map(function (type) { 107 | return { type: type }; 108 | }); 109 | 110 | // Auto-init 111 | if (this.__options.init) { 112 | this.init(); 113 | } 114 | } 115 | 116 | init() { 117 | if (this.__inited) return; 118 | this.__inited = true; 119 | 120 | // Install plugins 121 | this.install(this.__options.plugins); 122 | } 123 | 124 | destroy() { 125 | if (!this.__inited) return; 126 | if (this.__destroyed) return; 127 | this.__destroyed = true; 128 | 129 | // Notify of imminent destruction 130 | this.trigger({ type: "destroy" }); 131 | 132 | // Then uninstall plugins 133 | this.uninstall(); 134 | } 135 | 136 | frame() { 137 | this.events.map(this.trigger); 138 | } 139 | 140 | resolve(plugins) { 141 | plugins = Array.isArray(plugins) ? plugins : [plugins]; 142 | 143 | // Resolve alias database 144 | const o = this.__options; 145 | const aliases = Object.assign({}, o.aliasdb, o.aliases); 146 | 147 | // Remove inline alias defs from plugins 148 | const pred = function (name) { 149 | const key = name.split(":"); 150 | if (!key[1]) return true; 151 | aliases[key[0]] = [key[1]]; 152 | return false; 153 | }; 154 | plugins = plugins.filter(pred); 155 | 156 | // Unify arrays 157 | Object.entries(aliases).forEach(function ([key, alias]) { 158 | aliases[key] = Array.isArray(alias) ? alias : [alias]; 159 | }); 160 | 161 | // Look up aliases recursively 162 | function recurse(list, out, level) { 163 | if (level >= 256) throw "Plug-in alias recursion detected."; 164 | list = list.filter(pred); 165 | list.forEach(function (name) { 166 | const alias = aliases[name]; 167 | if (!alias) { 168 | out.push(name); 169 | } else { 170 | out = out.concat(recurse(alias, [], level + 1)); 171 | } 172 | }); 173 | return out; 174 | } 175 | 176 | return recurse(plugins, [], 0); 177 | } 178 | 179 | install(plugins) { 180 | plugins = Array.isArray(plugins) ? plugins : [plugins]; 181 | 182 | // Resolve aliases 183 | plugins = this.resolve(plugins); 184 | 185 | // Install in order 186 | each(plugins, (name) => this.__install(name)) 187 | 188 | // Fire off ready event 189 | this.__ready(); 190 | } 191 | 192 | uninstall(plugins) { 193 | if (plugins) { 194 | plugins = Array.isArray(plugins) ? plugins : [plugins]; 195 | 196 | // Resolve aliases 197 | plugins = this.resolve(plugins); 198 | } 199 | 200 | // Uninstall in reverse order 201 | (plugins || this.__installed) 202 | .reverse() 203 | .forEach((p) => this.__uninstall(p)); 204 | } 205 | 206 | __install(name) { 207 | // Sanity check 208 | const ctor = this.__options.plugindb[name]; 209 | if (!ctor) 210 | throw "[three.install] Cannot install. '" + name + "' is not registered."; 211 | 212 | if (this.plugins[name]) 213 | return console.warn("[three.install] " + name + " is already installed."); 214 | 215 | // Construct 216 | const Plugin = ctor; 217 | const plugin = new Plugin(this.__options[name] || {}, name); 218 | this.plugins[name] = plugin; 219 | 220 | // Install 221 | const flag = plugin.install(this); 222 | this.__installed.push(plugin); 223 | 224 | // Then notify 225 | this.trigger({ type: "install", plugin: plugin }); 226 | 227 | // Allow early abort 228 | return flag; 229 | } 230 | 231 | __uninstall(name) { 232 | // Sanity check 233 | const plugin = isString(name) ? this.plugins[name] : name; 234 | if (!plugin) { 235 | console.warn("[three.uninstall] " + name + "' is not installed."); 236 | return; 237 | } 238 | 239 | name = plugin.__name; 240 | 241 | // Uninstall 242 | plugin.uninstall(this); 243 | this.__installed = this.__installed.filter((p) => p !== plugin); 244 | delete this.plugins[name]; 245 | 246 | // Then notify 247 | this.trigger({ type: "uninstall", plugin: plugin }); 248 | } 249 | 250 | __ready() { 251 | // Notify and remove event handlers 252 | this.triggerOnce({ type: "ready" }); 253 | } 254 | } 255 | Bootstrap.initClass(); 256 | 257 | // Plugin Creation 258 | 259 | Bootstrap.Plugin = function (options) { 260 | this.options = Object.assign({}, this.defaults, options || {}); 261 | }; 262 | 263 | Bootstrap.Plugin.prototype = { 264 | listen: [], 265 | defaults: {}, 266 | install: function (_three) {}, 267 | uninstall: function (_three) {}, 268 | }; 269 | 270 | Binder.apply(Bootstrap.prototype); 271 | Binder.apply(Bootstrap.Plugin.prototype); 272 | Api.apply(Bootstrap.Plugin.prototype); 273 | -------------------------------------------------------------------------------- /src/controls/VRControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author dmarcos / https://github.com/dmarcos 3 | * @author mrdoob / http://mrdoob.com 4 | * 5 | * VRControls from 6 | * https://cdn.jsdelivr.net/npm/three@0.93.0/examples/js/controls/VRControls.js. 7 | * Added here so that the existing VR examples still work... this will stay 8 | * until we get everything upgraded to the modern three.js approach to VR. See 9 | * https://threejs.org/docs/index.html#manual/en/introduction/How-to-create-VR-content 10 | * for more info. 11 | */ 12 | 13 | import { Matrix4 } from "three"; 14 | 15 | export class VRControls { 16 | constructor(object, onError) { 17 | this.object = object; 18 | this.standingMatrix = new Matrix4(); 19 | this.frameData = null; 20 | 21 | if ("VRFrameData" in window) { 22 | // eslint-disable-next-line no-undef 23 | this.frameData = new VRFrameData(); 24 | } 25 | 26 | function gotVRDisplays(displays) { 27 | this.vrDisplays = displays; 28 | 29 | if (displays.length > 0) { 30 | this.vrDisplay = displays[0]; 31 | } else { 32 | if (onError) onError("VR input not available."); 33 | } 34 | } 35 | 36 | if (navigator.getVRDisplays) { 37 | navigator 38 | .getVRDisplays() 39 | .then(gotVRDisplays) 40 | .catch(function () { 41 | console.warn("VRControls: Unable to get VR Displays"); 42 | }); 43 | } 44 | 45 | // the Rift SDK returns the position in meters 46 | // this scale factor allows the user to define how meters 47 | // are converted to scene units. 48 | 49 | this.scale = 1; 50 | 51 | // If true will use "standing space" coordinate system where y=0 is the 52 | // floor and x=0, z=0 is the center of the room. 53 | this.standing = false; 54 | 55 | // Distance from the users eyes to the floor in meters. Used when 56 | // standing=true but the VRDisplay doesn't provide stageParameters. 57 | this.userHeight = 1.6; 58 | } 59 | 60 | getVRDisplay() { 61 | return this.vrDisplay; 62 | } 63 | 64 | setVRDisplay(value) { 65 | this.vrDisplay = value; 66 | } 67 | 68 | getVRDisplays() { 69 | console.warn("VRControls: getVRDisplays() is being deprecated."); 70 | return this.vrDisplays; 71 | } 72 | 73 | getStandingMatrix() { 74 | return this.standingMatrix; 75 | } 76 | 77 | update() { 78 | if (this.vrDisplay) { 79 | let pose; 80 | 81 | if (this.vrDisplay.getFrameData) { 82 | this.vrDisplay.getFrameData(this.frameData); 83 | pose = this.frameData.pose; 84 | } else if (this.vrDisplay.getPose) { 85 | pose = this.vrDisplay.getPose(); 86 | } 87 | 88 | if (pose.orientation !== null) { 89 | this.object.quaternion.fromArray(pose.orientation); 90 | } 91 | 92 | if (pose.position !== null) { 93 | this.object.position.fromArray(pose.position); 94 | } else { 95 | this.object.position.set(0, 0, 0); 96 | } 97 | 98 | if (this.standing) { 99 | if (this.vrDisplay.stageParameters) { 100 | this.object.updateMatrix(); 101 | 102 | this.standingMatrix.fromArray( 103 | this.vrDisplay.stageParameters.sittingToStandingTransform 104 | ); 105 | this.object.applyMatrix(this.standingMatrix); 106 | } else { 107 | this.object.position.setY(this.object.position.y + this.userHeight); 108 | } 109 | } 110 | 111 | this.object.position.multiplyScalar(this.scale); 112 | } 113 | } 114 | 115 | dispose() { 116 | this.vrDisplay = null; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/controls/index.js: -------------------------------------------------------------------------------- 1 | import "./VRControls.js"; 2 | -------------------------------------------------------------------------------- /src/core/bind.js: -------------------------------------------------------------------------------- 1 | import { Bootstrap } from "../bootstrap"; 2 | import { Binder } from "../binder"; 3 | 4 | Bootstrap.registerPlugin("bind", { 5 | install: function (three) { 6 | const globals = { 7 | three: three, 8 | window: window, 9 | }; 10 | 11 | three.bind = Binder.bind(three, globals); 12 | three.unbind = Binder.unbind(three); 13 | 14 | three.bind("install:bind", this); 15 | three.bind("uninstall:unbind", this); 16 | }, 17 | 18 | uninstall: function (three) { 19 | three.unbind(this); 20 | 21 | delete three.bind; 22 | delete three.unbind; 23 | }, 24 | 25 | bind: function (event, three) { 26 | const plugin = event.plugin; 27 | const listen = plugin.listen; 28 | 29 | listen && 30 | listen.forEach(function (key) { 31 | three.bind(key, plugin); 32 | }); 33 | }, 34 | 35 | unbind: function (event, three) { 36 | three.unbind(event.plugin); 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /src/core/camera.js: -------------------------------------------------------------------------------- 1 | import { Camera, OrthographicCamera, PerspectiveCamera } from "three"; 2 | import { Bootstrap } from "../bootstrap"; 3 | 4 | Bootstrap.registerPlugin("camera", { 5 | defaults: { 6 | near: 0.01, 7 | far: 10000, 8 | 9 | type: "perspective", 10 | fov: 60, 11 | aspect: null, 12 | 13 | // type: 'orthographic', 14 | left: -1, 15 | right: 1, 16 | bottom: -1, 17 | top: 1, 18 | 19 | klass: null, 20 | parameters: null, 21 | }, 22 | 23 | listen: ["resize", "this.change"], 24 | 25 | install: function (three) { 26 | three.Camera = this.api(); 27 | three.camera = null; 28 | 29 | this.aspect = 1; 30 | this.change({}, three); 31 | }, 32 | 33 | uninstall: function (three) { 34 | delete three.Camera; 35 | delete three.camera; 36 | }, 37 | 38 | change: function (event, three) { 39 | const o = this.options; 40 | const old = three.camera; 41 | 42 | if (!three.camera || event.changes.type || event.changes.klass) { 43 | const klass = 44 | o.klass || 45 | { 46 | perspective: PerspectiveCamera, 47 | orthographic: OrthographicCamera, 48 | }[o.type] || 49 | Camera; 50 | 51 | three.camera = o.parameters ? new klass(o.parameters) : new klass(); 52 | } 53 | 54 | Object.entries(o).forEach( 55 | function ([key]) { 56 | if (Object.prototype.hasOwnProperty.call(three.camera, key)) 57 | three.camera[key] = o[key]; 58 | }.bind(this) 59 | ); 60 | 61 | this.update(three); 62 | 63 | old === three.camera || 64 | three.trigger({ 65 | type: "camera", 66 | camera: three.camera, 67 | }); 68 | }, 69 | 70 | resize: function (event, three) { 71 | this.aspect = event.viewWidth / Math.max(1, event.viewHeight); 72 | 73 | this.update(three); 74 | }, 75 | 76 | update: function (three) { 77 | three.camera.aspect = this.options.aspect || this.aspect; 78 | three.camera.updateProjectionMatrix(); 79 | }, 80 | }); 81 | -------------------------------------------------------------------------------- /src/core/fallback.js: -------------------------------------------------------------------------------- 1 | import { Bootstrap } from "../bootstrap"; 2 | 3 | Bootstrap.registerPlugin("fallback", { 4 | defaults: { 5 | force: false, 6 | fill: true, 7 | begin: 8 | '
' + 10 | '
', 11 | end: "
", 12 | message: 13 | "This example requires WebGL
" + 14 | 'Visit get.webgl.org for more info', 15 | }, 16 | 17 | install: function (three) { 18 | let cnv, gl; 19 | try { 20 | cnv = document.createElement("canvas"); 21 | gl = cnv.getContext("webgl") || cnv.getContext("experimental-webgl"); 22 | if (!gl || this.options.force) { 23 | throw "WebGL unavailable."; 24 | } 25 | three.fallback = false; 26 | } catch (e) { 27 | const message = this.options.message; 28 | const begin = this.options.begin; 29 | const end = this.options.end; 30 | const fill = this.options.fill; 31 | 32 | const div = document.createElement("div"); 33 | div.innerHTML = begin + message + end; 34 | 35 | this.children = []; 36 | 37 | while (div.childNodes.length > 0) { 38 | this.children.push(div.firstChild); 39 | three.element.appendChild(div.firstChild); 40 | } 41 | 42 | if (fill) { 43 | three.install("fill"); 44 | } 45 | 46 | this.div = div; 47 | three.fallback = true; 48 | return false; // Abort install 49 | } 50 | }, 51 | 52 | uninstall: function (three) { 53 | if (this.children) { 54 | this.children.forEach(function (child) { 55 | child.parentNode.removeChild(child); 56 | }); 57 | this.children = null; 58 | } 59 | 60 | delete three.fallback; 61 | }, 62 | }); 63 | -------------------------------------------------------------------------------- /src/core/fill.js: -------------------------------------------------------------------------------- 1 | import { Bootstrap } from "../bootstrap"; 2 | 3 | Bootstrap.registerPlugin("fill", { 4 | defaults: { 5 | block: true, 6 | body: true, 7 | layout: true, 8 | }, 9 | 10 | install: function (three) { 11 | function is(element) { 12 | const h = element.style.height; 13 | return h == "auto" || h == ""; 14 | } 15 | 16 | function set(element) { 17 | element.style.height = "100%"; 18 | element.style.margin = 0; 19 | element.style.padding = 0; 20 | return element; 21 | } 22 | 23 | if (this.options.body && three.element == document.body) { 24 | // Fix body height if we're naked 25 | this.applied = [three.element, document.documentElement] 26 | .filter(is) 27 | .map(set); 28 | } 29 | 30 | if (this.options.block && three.canvas) { 31 | three.canvas.style.display = "block"; 32 | this.block = true; 33 | } 34 | 35 | if (this.options.layout && three.element) { 36 | const style = window.getComputedStyle(three.element); 37 | if (style.position == "static") { 38 | three.element.style.position = "relative"; 39 | this.layout = true; 40 | } 41 | } 42 | }, 43 | 44 | uninstall: function (three) { 45 | if (this.applied) { 46 | const set = function (element) { 47 | element.style.height = ""; 48 | element.style.margin = ""; 49 | element.style.padding = ""; 50 | return element; 51 | }; 52 | 53 | this.applied.map(set); 54 | delete this.applied; 55 | } 56 | 57 | if (this.block && three.canvas) { 58 | three.canvas.style.display = ""; 59 | delete this.block; 60 | } 61 | 62 | if (this.layout && three.element) { 63 | three.element.style.position = ""; 64 | delete this.layout; 65 | } 66 | }, 67 | 68 | change: function (three) { 69 | this.uninstall(three); 70 | this.install(three); 71 | }, 72 | }); 73 | -------------------------------------------------------------------------------- /src/core/index.js: -------------------------------------------------------------------------------- 1 | import "./bind"; 2 | import "./camera"; 3 | import "./fallback"; 4 | import "./fill"; 5 | import "./loop"; 6 | import "./render"; 7 | import "./renderer"; 8 | import "./scene"; 9 | import "./size"; 10 | import "./time"; 11 | import "./warmup"; 12 | -------------------------------------------------------------------------------- /src/core/loop.js: -------------------------------------------------------------------------------- 1 | import { Bootstrap } from "../bootstrap"; 2 | 3 | Bootstrap.registerPlugin("loop", { 4 | defaults: { 5 | start: true, 6 | each: 1, 7 | }, 8 | 9 | listen: ["ready"], 10 | 11 | install: function (three) { 12 | this.running = false; 13 | this.lastRequestId = null; 14 | 15 | three.Loop = this.api( 16 | { 17 | start: this.start.bind(this), 18 | stop: this.stop.bind(this), 19 | running: false, 20 | window: window, 21 | }, 22 | three 23 | ); 24 | 25 | this.events = ["pre", "update", "render", "post"].map(function (type) { 26 | return { type: type }; 27 | }); 28 | }, 29 | 30 | uninstall: function (three) { 31 | this.stop(three); 32 | }, 33 | 34 | ready: function (event, three) { 35 | if (this.options.start) this.start(three); 36 | }, 37 | 38 | start: function (three) { 39 | if (this.running) return; 40 | 41 | three.Loop.running = this.running = true; 42 | 43 | const trigger = three.trigger.bind(three); 44 | const loop = function () { 45 | if (!this.running) return; 46 | this.lastRequestId = three.Loop.window.requestAnimationFrame(loop); 47 | this.events.map(trigger); 48 | }.bind(this); 49 | 50 | this.lastRequestId = three.Loop.window.requestAnimationFrame(loop); 51 | 52 | three.trigger({ type: "start" }); 53 | }, 54 | 55 | stop: function (three) { 56 | if (!this.running) return; 57 | three.Loop.running = this.running = false; 58 | 59 | three.Loop.window.cancelAnimationFrame(this.lastRequestId); 60 | this.lastRequestId = null; 61 | 62 | three.trigger({ type: "stop" }); 63 | }, 64 | }); 65 | -------------------------------------------------------------------------------- /src/core/render.js: -------------------------------------------------------------------------------- 1 | import { Bootstrap } from "../bootstrap"; 2 | 3 | Bootstrap.registerPlugin("render", { 4 | listen: ["render"], 5 | 6 | render: function (event, three) { 7 | if (three.scene && three.camera) { 8 | three.renderer.render(three.scene, three.camera); 9 | } 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/core/renderer.js: -------------------------------------------------------------------------------- 1 | import { WebGL1Renderer } from "three"; 2 | import { Bootstrap } from "../bootstrap"; 3 | 4 | Bootstrap.registerPlugin("renderer", { 5 | defaults: { 6 | klass: WebGL1Renderer, 7 | parameters: { 8 | depth: true, 9 | stencil: true, 10 | preserveDrawingBuffer: true, 11 | antialias: true, 12 | }, 13 | }, 14 | 15 | listen: ["resize"], 16 | 17 | install: function (three) { 18 | // Instantiate Three renderer 19 | const renderer = (three.renderer = new this.options.klass( 20 | this.options.parameters 21 | )); 22 | three.canvas = renderer.domElement; 23 | 24 | // Add to DOM 25 | three.element.appendChild(renderer.domElement); 26 | }, 27 | 28 | uninstall: function (three) { 29 | // Remove from DOM 30 | three.element.removeChild(three.renderer.domElement); 31 | 32 | delete three.renderer; 33 | delete three.canvas; 34 | }, 35 | 36 | resize: function (event, three) { 37 | const renderer = three.renderer; 38 | const el = renderer.domElement; 39 | 40 | // Resize renderer to render size if it's a canvas 41 | if (el && el.tagName == "CANVAS") { 42 | renderer.setSize(event.renderWidth, event.renderHeight, false); 43 | } 44 | // Or view size if it's just a DOM element or multi-renderer 45 | else { 46 | if (renderer.setRenderSize) { 47 | renderer.setRenderSize(event.renderWidth, event.renderHeight); 48 | } 49 | renderer.setSize(event.viewWidth, event.viewHeight, false); 50 | } 51 | }, 52 | }); 53 | -------------------------------------------------------------------------------- /src/core/scene.js: -------------------------------------------------------------------------------- 1 | import { Scene } from "three"; 2 | import { Bootstrap } from "../bootstrap"; 3 | 4 | Bootstrap.registerPlugin("scene", { 5 | install: function (three) { 6 | three.scene = new Scene(); 7 | }, 8 | 9 | uninstall: function (three) { 10 | delete three.scene; 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /src/core/size.js: -------------------------------------------------------------------------------- 1 | import { Bootstrap } from "../bootstrap"; 2 | 3 | Bootstrap.registerPlugin("size", { 4 | defaults: { 5 | width: null, 6 | height: null, 7 | aspect: null, 8 | scale: 1, 9 | maxRenderWidth: Infinity, 10 | maxRenderHeight: Infinity, 11 | devicePixelRatio: true, 12 | }, 13 | 14 | listen: [ 15 | "window.resize:queue", 16 | "element.resize:queue", 17 | "this.change:queue", 18 | "ready:resize", 19 | "pre:pre", 20 | ], 21 | 22 | install: function (three) { 23 | three.Size = this.api({ 24 | renderWidth: 0, 25 | renderHeight: 0, 26 | viewWidth: 0, 27 | viewHeight: 0, 28 | }); 29 | 30 | this.resized = false; 31 | }, 32 | 33 | uninstall: function (three) { 34 | delete three.Size; 35 | }, 36 | 37 | queue: function (_event, _three) { 38 | this.resized = true; 39 | }, 40 | 41 | pre: function (event, three) { 42 | if (!this.resized) return; 43 | this.resized = false; 44 | this.resize(event, three); 45 | }, 46 | 47 | resize: function (event, three) { 48 | const options = this.options; 49 | const element = three.element; 50 | const renderer = three.renderer; 51 | 52 | let w, 53 | h, 54 | ew, 55 | eh, 56 | rw, 57 | rh, 58 | aspect, 59 | ratio, 60 | ml = 0, 61 | mt = 0; 62 | 63 | // Measure element 64 | w = ew = 65 | options.width === undefined || options.width == null 66 | ? element.offsetWidth || element.innerWidth || 0 67 | : options.width; 68 | 69 | h = eh = 70 | options.height === undefined || options.height == null 71 | ? element.offsetHeight || element.innerHeight || 0 72 | : options.height; 73 | 74 | // Force aspect ratio 75 | aspect = w / h; 76 | if (options.aspect) { 77 | if (options.aspect > aspect) { 78 | h = Math.round(w / options.aspect); 79 | mt = Math.floor((eh - h) / 2); 80 | } else { 81 | w = Math.round(h * options.aspect); 82 | ml = Math.floor((ew - w) / 2); 83 | } 84 | aspect = w / h; 85 | } 86 | 87 | // Get device pixel ratio 88 | ratio = 1; 89 | if (options.devicePixelRatio && typeof window != "undefined") { 90 | ratio = window.devicePixelRatio || 1; 91 | } 92 | 93 | // Apply scale and resolution max 94 | rw = Math.round( 95 | Math.min(w * ratio * options.scale, options.maxRenderWidth) 96 | ); 97 | rh = Math.round( 98 | Math.min(h * ratio * options.scale, options.maxRenderHeight) 99 | ); 100 | 101 | // Retain aspect ratio 102 | const raspect = rw / rh; 103 | if (raspect > aspect) { 104 | rw = Math.round(rh * aspect); 105 | } else { 106 | rh = Math.round(rw / aspect); 107 | } 108 | 109 | // Measure final pixel ratio 110 | ratio = rh / h; 111 | 112 | // Resize and position renderer element 113 | const style = renderer.domElement.style; 114 | style.width = w + "px"; 115 | style.height = h + "px"; 116 | style.marginLeft = ml + "px"; 117 | style.marginTop = mt + "px"; 118 | 119 | // Notify 120 | Object.assign(three.Size, { 121 | renderWidth: rw, 122 | renderHeight: rh, 123 | viewWidth: w, 124 | viewHeight: h, 125 | aspect: aspect, 126 | pixelRatio: ratio, 127 | }); 128 | 129 | three.trigger({ 130 | type: "resize", 131 | renderWidth: rw, 132 | renderHeight: rh, 133 | viewWidth: w, 134 | viewHeight: h, 135 | aspect: aspect, 136 | pixelRatio: ratio, 137 | }); 138 | }, 139 | }); 140 | -------------------------------------------------------------------------------- /src/core/time.js: -------------------------------------------------------------------------------- 1 | import { Bootstrap } from "../bootstrap"; 2 | 3 | Bootstrap.registerPlugin("time", { 4 | defaults: { 5 | speed: 1, // Clock speed 6 | warmup: 0, // Wait N frames before starting clock 7 | timeout: 1, // Timeout in seconds. Pause if no tick happens in this time. 8 | }, 9 | 10 | listen: ["pre:tick", "this.change"], 11 | 12 | now: function () { 13 | return +new Date() / 1000; 14 | }, 15 | 16 | install: function (three) { 17 | three.Time = this.api({ 18 | now: this.now(), // Time since 1970 in seconds 19 | 20 | clock: 0, // Adjustable clock that counts up from 0 seconds 21 | step: 1 / 60, // Clock step in seconds 22 | 23 | frames: 0, // Framenumber 24 | time: 0, // Real time in seconds 25 | delta: 1 / 60, // Frame step in seconds 26 | 27 | average: 0, // Average frame time in seconds 28 | fps: 0, // Average frames per second 29 | }); 30 | 31 | this.last = 0; 32 | this.time = 0; 33 | this.clock = 0; 34 | this.wait = this.options.warmup; 35 | 36 | this.clockStart = 0; 37 | this.timeStart = 0; 38 | }, 39 | 40 | tick: function (event, three) { 41 | const speed = this.options.speed; 42 | const timeout = this.options.timeout; 43 | 44 | const api = three.Time; 45 | const now = (api.now = this.now()); 46 | const last = this.last; 47 | let time = this.time; 48 | let clock = this.clock; 49 | 50 | if (last) { 51 | let delta = (api.delta = now - last); 52 | const average = api.average || delta; 53 | 54 | if (delta > timeout) { 55 | delta = 0; 56 | } 57 | 58 | const step = delta * speed; 59 | 60 | time += delta; 61 | clock += step; 62 | 63 | if (api.frames > 0) { 64 | api.average = average + (delta - average) * 0.1; 65 | api.fps = 1 / average; 66 | } 67 | 68 | api.step = step; 69 | api.clock = clock - this.clockStart; 70 | api.time = time - this.timeStart; 71 | 72 | api.frames++; 73 | 74 | if (this.wait-- > 0) { 75 | this.clockStart = clock; 76 | this.timeStart = time; 77 | api.clock = 0; 78 | api.step = 1e-100; 79 | } 80 | } 81 | 82 | this.last = now; 83 | this.clock = clock; 84 | this.time = time; 85 | }, 86 | 87 | uninstall: function (three) { 88 | delete three.Time; 89 | }, 90 | }); 91 | -------------------------------------------------------------------------------- /src/core/warmup.js: -------------------------------------------------------------------------------- 1 | import { Bootstrap } from "../bootstrap"; 2 | 3 | Bootstrap.registerPlugin("warmup", { 4 | defaults: { 5 | delay: 2, 6 | }, 7 | 8 | listen: ["ready", "post"], 9 | 10 | ready: function (event, three) { 11 | three.renderer.domElement.style.visibility = "hidden"; 12 | this.frame = 0; 13 | this.hidden = true; 14 | }, 15 | 16 | post: function (event, three) { 17 | if (this.hidden && this.frame >= this.options.delay) { 18 | three.renderer.domElement.style.visibility = "visible"; 19 | this.hidden = false; 20 | } 21 | this.frame++; 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /src/extra/controls.js: -------------------------------------------------------------------------------- 1 | import { PerspectiveCamera } from "three"; 2 | import { Bootstrap } from "../bootstrap"; 3 | 4 | Bootstrap.registerPlugin("controls", { 5 | listen: ["update", "resize", "camera", "this.change"], 6 | 7 | defaults: { 8 | klass: null, 9 | parameters: {}, 10 | }, 11 | 12 | install: function (three) { 13 | if (!this.options.klass) throw "Must provide class for `controls.klass`"; 14 | 15 | three.controls = null; 16 | 17 | this._camera = three.camera || new PerspectiveCamera(); 18 | this.change(null, three); 19 | }, 20 | 21 | uninstall: function (three) { 22 | delete three.controls; 23 | }, 24 | 25 | change: function (event, three) { 26 | if (this.options.klass) { 27 | if (!event || event.changes.klass) { 28 | three.controls = new this.options.klass( 29 | this._camera, 30 | three.renderer.domElement 31 | ); 32 | } 33 | 34 | Object.assign(three.controls, this.options.parameters); 35 | } else { 36 | three.controls = null; 37 | } 38 | }, 39 | 40 | update: function (event, three) { 41 | const delta = (three.Time && three.Time.delta) || 1 / 60; 42 | const vr = three.VR && three.VR.state; 43 | 44 | if (three.controls.vr) three.controls.vr(vr); 45 | three.controls.update(delta); 46 | }, 47 | 48 | camera: function (event, three) { 49 | three.controls.object = this._camera = event.camera; 50 | }, 51 | 52 | resize: function (event, three) { 53 | three.controls.handleResize && three.controls.handleResize(); 54 | }, 55 | }); 56 | -------------------------------------------------------------------------------- /src/extra/cursor.js: -------------------------------------------------------------------------------- 1 | import { Bootstrap } from "../bootstrap"; 2 | 3 | Bootstrap.registerPlugin("cursor", { 4 | listen: [ 5 | "update", 6 | "this.change", 7 | "install:change", 8 | "uninstall:change", 9 | "element.mousemove", 10 | "vr", 11 | ], 12 | 13 | defaults: { 14 | cursor: null, 15 | hide: false, 16 | timeout: 3, 17 | }, 18 | 19 | install: function (three) { 20 | this.timeout = this.options.timeout; 21 | this.element = three.element; 22 | this.change(null, three); 23 | }, 24 | 25 | uninstall: function (three) { 26 | delete three.controls; 27 | }, 28 | 29 | change: function (event, three) { 30 | this.applyCursor(three); 31 | }, 32 | 33 | mousemove: function (event, three) { 34 | if (this.options.hide) { 35 | this.applyCursor(three); 36 | this.timeout = +this.options.timeout || 0; 37 | } 38 | }, 39 | 40 | update: function (event, three) { 41 | const delta = (three.Time && three.Time.delta) || 1 / 60; 42 | 43 | if (this.options.hide) { 44 | this.timeout -= delta; 45 | if (this.timeout < 0) { 46 | this.applyCursor(three, "none"); 47 | } 48 | } 49 | }, 50 | 51 | vr: function (event, three) { 52 | this.hide = event.active && !event.hmd.fake; 53 | this.applyCursor(three); 54 | }, 55 | 56 | applyCursor: function (three, cursor) { 57 | const auto = three.controls ? "move" : ""; 58 | cursor = cursor || this.options.cursor || auto; 59 | if (this.hide) cursor = "none"; 60 | if (this.cursor != cursor) { 61 | this.element.style.cursor = cursor; 62 | } 63 | }, 64 | }); 65 | -------------------------------------------------------------------------------- /src/extra/fullscreen.js: -------------------------------------------------------------------------------- 1 | import { Bootstrap } from "../bootstrap"; 2 | 3 | Bootstrap.registerPlugin("fullscreen", { 4 | defaults: { 5 | key: "f", 6 | }, 7 | 8 | listen: ["ready", "update"], 9 | 10 | install: function (three) { 11 | three.Fullscreen = this.api( 12 | { 13 | active: false, 14 | toggle: this.toggle.bind(this), 15 | }, 16 | three 17 | ); 18 | }, 19 | 20 | uninstall: function (three) { 21 | delete three.Fullscreen; 22 | }, 23 | 24 | ready: function (event, three) { 25 | document.body.addEventListener( 26 | "keypress", 27 | function (event) { 28 | if ( 29 | this.options.key && 30 | event.charCode == this.options.key.charCodeAt(0) 31 | ) { 32 | this.toggle(three); 33 | } 34 | }.bind(this) 35 | ); 36 | 37 | const changeHandler = function () { 38 | const active = 39 | !!document.fullscreenElement || 40 | !!document.mozFullScreenElement || 41 | !!document.webkitFullscreenElement || 42 | !!document.msFullscreenElement; 43 | three.Fullscreen.active = this.active = active; 44 | three.trigger({ 45 | type: "fullscreen", 46 | active: active, 47 | }); 48 | }.bind(this); 49 | document.addEventListener("fullscreenchange", changeHandler, false); 50 | document.addEventListener("webkitfullscreenchange", changeHandler, false); 51 | document.addEventListener("mozfullscreenchange", changeHandler, false); 52 | }, 53 | 54 | toggle: function (three) { 55 | const canvas = three.canvas; 56 | const options = 57 | three.VR && three.VR.active ? { vrDisplay: three.VR.hmd } : {}; 58 | 59 | if (!this.active) { 60 | if (canvas.requestFullScreen) { 61 | canvas.requestFullScreen(options); 62 | } else if (canvas.msRequestFullScreen) { 63 | canvas.msRequestFullscreen(options); 64 | } else if (canvas.webkitRequestFullscreen) { 65 | canvas.webkitRequestFullscreen(options); 66 | } else if (canvas.mozRequestFullScreen) { 67 | canvas.mozRequestFullScreen(options); // s vs S 68 | } 69 | } else { 70 | if (document.exitFullscreen) { 71 | document.exitFullscreen(); 72 | } else if (document.msExitFullscreen) { 73 | document.msExitFullscreen(); 74 | } else if (document.webkitExitFullscreen) { 75 | document.webkitExitFullscreen(); 76 | } else if (document.mozCancelFullScreen) { 77 | document.mozCancelFullScreen(); // s vs S 78 | } 79 | } 80 | }, 81 | }); 82 | -------------------------------------------------------------------------------- /src/extra/index.js: -------------------------------------------------------------------------------- 1 | import "./controls"; 2 | import "./cursor"; 3 | import "./fullscreen"; 4 | import "./stats"; 5 | import "./ui"; 6 | import "./vr"; 7 | -------------------------------------------------------------------------------- /src/extra/stats.js: -------------------------------------------------------------------------------- 1 | import { default as Stats } from "stats.js"; 2 | import { Bootstrap } from "../bootstrap"; 3 | 4 | Bootstrap.registerPlugin("stats", { 5 | listen: ["pre", "post"], 6 | 7 | install: function (three) { 8 | const stats = (this.stats = new Stats()); 9 | const style = stats.domElement.style; 10 | 11 | style.position = "absolute"; 12 | style.top = style.left = 0; 13 | three.element.appendChild(stats.domElement); 14 | 15 | three.stats = stats; 16 | }, 17 | 18 | uninstall: function (three) { 19 | this.stats.domElement.remove() 20 | delete three.stats; 21 | }, 22 | 23 | pre: function (_event, _three) { 24 | this.stats.begin(); 25 | }, 26 | 27 | post: function (_event, _three) { 28 | this.stats.end(); 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /src/extra/ui.js: -------------------------------------------------------------------------------- 1 | import { Bootstrap } from "../bootstrap"; 2 | 3 | Bootstrap.registerPlugin("ui", { 4 | defaults: { 5 | theme: "white", 6 | style: 7 | ".threestrap-ui { position: absolute; bottom: 5px; right: 5px; float: left; }" + 8 | ".threestrap-ui button { border: 0; background: none;" + 9 | " vertical-align: middle; font-weight: bold; } " + 10 | ".threestrap-ui .glyphicon { top: 2px; font-weight: bold; } " + 11 | "@media (max-width: 640px) { .threestrap-ui button { font-size: 120% } }" + 12 | ".threestrap-white button { color: #fff; text-shadow: 0 1px 1px rgba(0, 0, 0, 1), " + 13 | "0 1px 3px rgba(0, 0, 0, 1); }" + 14 | ".threestrap-black button { color: #000; text-shadow: 0 0px 1px rgba(255, 255, 255, 1), " + 15 | "0 0px 2px rgba(255, 255, 255, 1), " + 16 | "0 0px 2px rgba(255, 255, 255, 1) }", 17 | }, 18 | 19 | listen: ["fullscreen"], 20 | 21 | markup: function (three, theme, style) { 22 | let url = 23 | "//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-glyphicons.css"; 24 | if (location.href.match(/^file:\/\//)) url = "http://" + url; 25 | 26 | const buttons = []; 27 | 28 | if (three.Fullscreen) { 29 | buttons.push( 30 | '" 33 | ); 34 | } 35 | if (three.VR) { 36 | buttons.push(''); 37 | } 38 | 39 | return ( 40 | '" + 45 | '
' + 48 | buttons.join("\n") + 49 | "
" 50 | ); 51 | }, 52 | 53 | install: function (three) { 54 | const ui = (this.ui = document.createElement("div")); 55 | ui.innerHTML = this.markup(three, this.options.theme, this.options.style); 56 | document.body.appendChild(ui); 57 | 58 | const fullscreen = (this.ui.fullscreen = 59 | ui.querySelector("button.fullscreen")); 60 | if (fullscreen) { 61 | three.bind([fullscreen, "click:goFullscreen"], this); 62 | } 63 | 64 | const vr = (this.ui.vr = ui.querySelector("button.vr")); 65 | if (vr && three.VR) { 66 | three.VR.set({ mode: "2d" }); 67 | three.bind([vr, "click:goVR"], this); 68 | } 69 | }, 70 | 71 | fullscreen: function (event, three) { 72 | this.ui.style.display = event.active ? "none" : "block"; 73 | if (!event.active) three.VR && three.VR.set({ mode: "2d" }); 74 | }, 75 | 76 | goFullscreen: function (event, three) { 77 | if (three.Fullscreen) { 78 | three.Fullscreen.toggle(); 79 | } 80 | }, 81 | 82 | goVR: function (event, three) { 83 | if (three.VR) { 84 | three.VR.set({ mode: "auto" }); 85 | three.Fullscreen.toggle(); 86 | } 87 | }, 88 | 89 | uninstall: function (_three) { 90 | document.body.removeChild(this.ui); 91 | }, 92 | }); 93 | -------------------------------------------------------------------------------- /src/extra/vr.js: -------------------------------------------------------------------------------- 1 | import { Bootstrap } from "../bootstrap.js"; 2 | import { VRRenderer } from "../renderers/VRRenderer.js"; 3 | 4 | /* 5 | VR sensor / HMD hookup. 6 | */ 7 | Bootstrap.registerPlugin("vr", { 8 | defaults: { 9 | mode: "auto", // 'auto', '2d' 10 | device: null, 11 | fov: 80, // emulated FOV for fallback 12 | }, 13 | 14 | listen: ["window.load", "pre", "render", "resize", "this.change"], 15 | 16 | install: function (three) { 17 | three.VR = this.api( 18 | { 19 | active: false, 20 | devices: [], 21 | hmd: null, 22 | sensor: null, 23 | renderer: null, 24 | state: null, 25 | }, 26 | three 27 | ); 28 | }, 29 | 30 | uninstall: function (three) { 31 | delete three.VR; 32 | }, 33 | 34 | mocks: function (three, fov, def) { 35 | // Fake VR device for cardboard / desktop 36 | 37 | // Interpuppilary distance 38 | const ipd = 0.03; 39 | 40 | // Symmetric eye FOVs (Cardboard style) 41 | const getEyeTranslation = function (key) { 42 | return { left: { x: -ipd, y: 0, z: 0 }, right: { x: ipd, y: 0, z: 0 } }[ 43 | key 44 | ]; 45 | }; 46 | const getRecommendedEyeFieldOfView = function (key) { 47 | const camera = three.camera; 48 | const aspect = (camera && camera.aspect) || 16 / 9; 49 | const fov2 = (fov || (camera && camera.fov) || def) / 2; 50 | const fovX = 51 | (Math.atan((Math.tan((fov2 * Math.PI) / 180) * aspect) / 2) * 180) / 52 | Math.PI; 53 | const fovY = fov2; 54 | 55 | return { 56 | left: { 57 | rightDegrees: fovX, 58 | leftDegrees: fovX, 59 | downDegrees: fovY, 60 | upDegrees: fovY, 61 | }, 62 | right: { 63 | rightDegrees: fovX, 64 | leftDegrees: fovX, 65 | downDegrees: fovY, 66 | upDegrees: fovY, 67 | }, 68 | }[key]; 69 | }; 70 | // Will be replaced with orbit controls or device orientation controls by VRControls 71 | const getState = function () { 72 | return {}; 73 | }; 74 | 75 | return [ 76 | { 77 | fake: true, 78 | force: 1, 79 | deviceId: "emu", 80 | deviceName: "Emulated", 81 | getEyeTranslation: getEyeTranslation, 82 | getRecommendedEyeFieldOfView: getRecommendedEyeFieldOfView, 83 | }, 84 | { 85 | force: 2, 86 | getState: getState, 87 | }, 88 | ]; 89 | }, 90 | 91 | load: function (event, three) { 92 | const callback = function (devs) { 93 | this.callback(devs, three); 94 | }.bind(this); 95 | 96 | if (navigator.getVRDevices) { 97 | navigator.getVRDevices().then(callback); 98 | } else if (navigator.mozGetVRDevices) { 99 | navigator.mozGetVRDevices(callback); 100 | } else { 101 | console.warn("No native VR support detected."); 102 | callback(this.mocks(three, this.options.fov, this.defaults.fov), three); 103 | } 104 | }, 105 | 106 | callback: function (vrdevs, three) { 107 | let hmd, sensor; 108 | 109 | const HMD = window.HMDVRDevice || function () {}; 110 | const SENSOR = window.PositionSensorVRDevice || function () {}; 111 | 112 | // Export list of devices 113 | vrdevs = three.VR.devices = vrdevs || three.VR.devices; 114 | 115 | // Get HMD device 116 | const deviceId = this.options.device; 117 | let dev; 118 | 119 | for (let i = 0; i < vrdevs.length; ++i) { 120 | dev = vrdevs[i]; 121 | if (dev.force == 1 || dev instanceof HMD) { 122 | if (deviceId && deviceId != dev.deviceId) continue; 123 | hmd = dev; 124 | break; 125 | } 126 | } 127 | 128 | if (hmd) { 129 | // Get sensor device 130 | let dev; 131 | for (let i = 0; i < vrdevs.length; ++i) { 132 | dev = vrdevs[i]; 133 | if ( 134 | dev.force == 2 || 135 | (dev instanceof SENSOR && dev.hardwareUnitId == hmd.hardwareUnitId) 136 | ) { 137 | sensor = dev; 138 | break; 139 | } 140 | } 141 | 142 | this.hookup(hmd, sensor, three); 143 | } 144 | }, 145 | 146 | hookup: function (hmd, sensor, three) { 147 | if (!VRRenderer) console.log("VRRenderer not found"); 148 | const klass = VRRenderer || function () {}; 149 | 150 | this.renderer = new klass(three.renderer, hmd); 151 | this.hmd = hmd; 152 | this.sensor = sensor; 153 | 154 | three.VR.renderer = this.renderer; 155 | three.VR.hmd = hmd; 156 | three.VR.sensor = sensor; 157 | 158 | console.log("VRRenderer", hmd.deviceName); 159 | }, 160 | 161 | change: function (event, three) { 162 | if (event.changes.device) { 163 | this.callback(null, three); 164 | } 165 | this.pre(event, three); 166 | }, 167 | 168 | pre: function (event, three) { 169 | const last = this.active; 170 | 171 | // Global active flag 172 | const active = (this.active = this.renderer && this.options.mode != "2d"); 173 | three.VR.active = active; 174 | 175 | // Load sensor state 176 | if (active && this.sensor) { 177 | const state = this.sensor.getState(); 178 | three.VR.state = state; 179 | } else { 180 | three.VR.state = null; 181 | } 182 | 183 | // Notify if VR state changed 184 | if (last != this.active) { 185 | three.trigger({ 186 | type: "vr", 187 | active: active, 188 | hmd: this.hmd, 189 | sensor: this.sensor, 190 | }); 191 | } 192 | }, 193 | 194 | resize: function (_event, _three) { 195 | if (this.active) { 196 | // Reinit HMD projection 197 | this.renderer.initialize(); 198 | } 199 | }, 200 | 201 | render: function (event, three) { 202 | if (three.scene && three.camera) { 203 | const renderer = this.active ? this.renderer : three.renderer; 204 | 205 | if (this.last != renderer) { 206 | if (renderer == three.renderer) { 207 | // Cleanup leftover renderer state when swapping back to normal 208 | const dpr = renderer.getPixelRatio(); 209 | const width = renderer.domElement.width / dpr; 210 | const height = renderer.domElement.height / dpr; 211 | renderer.setScissorTest(false); 212 | renderer.setViewport(0, 0, width, height); 213 | } 214 | } 215 | 216 | this.last = renderer; 217 | 218 | renderer.render(three.scene, three.camera); 219 | } 220 | }, 221 | }); 222 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import "./aliases"; 2 | import "./binder"; 3 | import "./bootstrap"; 4 | import "./core"; 5 | import "./extra"; 6 | 7 | // These should probably be in their own build! 8 | import "./controls"; 9 | import "./renderers"; 10 | 11 | export { Api } from "./api.js"; 12 | export { Binder } from "./binder.js"; 13 | export { Bootstrap } from "./bootstrap.js"; 14 | 15 | export { VRControls } from "./controls/VRControls.js"; 16 | export { MultiRenderer } from "./renderers/MultiRenderer.js"; 17 | export { VRRenderer } from "./renderers/VRRenderer.js"; 18 | -------------------------------------------------------------------------------- /src/renderers/MultiRenderer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Allows a stack of renderers to be treated as a single renderer. 3 | * @author Gheric Speiginer 4 | */ 5 | import { REVISION } from "three"; 6 | 7 | export class MultiRenderer { 8 | constructor(parameters) { 9 | console.log("MultiRenderer", REVISION); 10 | 11 | this.domElement = document.createElement("div"); 12 | this.domElement.style.position = "relative"; 13 | 14 | this.renderers = []; 15 | this._renderSizeSet = false; 16 | 17 | const rendererClasses = parameters.renderers || []; 18 | const rendererParameters = parameters.parameters || []; 19 | 20 | // elements are stacked back-to-front 21 | for (let i = 0; i < rendererClasses.length; i++) { 22 | const renderer = new rendererClasses[i](rendererParameters[i]); 23 | renderer.domElement.style.position = "absolute"; 24 | renderer.domElement.style.top = "0px"; 25 | renderer.domElement.style.left = "0px"; 26 | this.domElement.appendChild(renderer.domElement); 27 | this.renderers.push(renderer); 28 | } 29 | } 30 | 31 | setSize(w, h) { 32 | this.domElement.style.width = w + "px"; 33 | this.domElement.style.height = h + "px"; 34 | 35 | for (let i = 0; i < this.renderers.length; i++) { 36 | const renderer = this.renderers[i]; 37 | const el = renderer.domElement; 38 | 39 | if (!this._renderSizeSet || (el && el.tagName !== "CANVAS")) { 40 | renderer.setSize(w, h); 41 | } 42 | 43 | el.style.width = w + "px"; 44 | el.style.height = h + "px"; 45 | } 46 | } 47 | 48 | setRenderSize(rw, rh) { 49 | this._renderSizeSet = true; 50 | 51 | for (let i = 0; i < this.renderers.length; i++) { 52 | const renderer = this.renderers[i]; 53 | const el = renderer.domElement; 54 | 55 | if (el && el.tagName === "CANVAS") { 56 | renderer.setSize(rw, rh, false); 57 | } 58 | } 59 | } 60 | 61 | render(scene, camera) { 62 | for (let i = 0; i < this.renderers.length; i++) { 63 | this.renderers[i].render(scene, camera); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/renderers/VRRenderer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * VRRenderer 3 | * 4 | * @author wwwtyro https://github.com/wwwtyro 5 | * @author unconed https://github.com/unconed 6 | */ 7 | import { PerspectiveCamera, Vector3 } from "three"; 8 | 9 | export class VRRenderer { 10 | constructor(renderer, hmd) { 11 | this.renderer = renderer; 12 | 13 | this.right = new Vector3(); 14 | this.cameraLeft = new PerspectiveCamera(); 15 | this.cameraRight = new PerspectiveCamera(); 16 | 17 | const et = hmd.getEyeTranslation("left"); 18 | this.halfIPD = new Vector3(et.x, et.y, et.z).length(); 19 | this.fovLeft = hmd.getRecommendedEyeFieldOfView("left"); 20 | this.fovRight = hmd.getRecommendedEyeFieldOfView("right"); 21 | } 22 | 23 | FovToNDCScaleOffset(fov) { 24 | const pxscale = 2.0 / (fov.leftTan + fov.rightTan); 25 | const pxoffset = (fov.leftTan - fov.rightTan) * pxscale * 0.5; 26 | const pyscale = 2.0 / (fov.upTan + fov.downTan); 27 | const pyoffset = (fov.upTan - fov.downTan) * pyscale * 0.5; 28 | return { 29 | scale: [pxscale, pyscale], 30 | offset: [pxoffset, pyoffset], 31 | }; 32 | } 33 | 34 | FovPortToProjection( 35 | matrix, 36 | fov, 37 | rightHanded /* = true */, 38 | zNear /* = 0.01 */, 39 | zFar /* = 10000.0 */ 40 | ) { 41 | rightHanded = rightHanded === undefined ? true : rightHanded; 42 | zNear = zNear === undefined ? 0.01 : zNear; 43 | zFar = zFar === undefined ? 10000.0 : zFar; 44 | const handednessScale = rightHanded ? -1.0 : 1.0; 45 | const m = matrix.elements; 46 | const scaleAndOffset = this.FovToNDCScaleOffset(fov); 47 | m[0 * 4 + 0] = scaleAndOffset.scale[0]; 48 | m[0 * 4 + 1] = 0.0; 49 | m[0 * 4 + 2] = scaleAndOffset.offset[0] * handednessScale; 50 | m[0 * 4 + 3] = 0.0; 51 | m[1 * 4 + 0] = 0.0; 52 | m[1 * 4 + 1] = scaleAndOffset.scale[1]; 53 | m[1 * 4 + 2] = -scaleAndOffset.offset[1] * handednessScale; 54 | m[1 * 4 + 3] = 0.0; 55 | m[2 * 4 + 0] = 0.0; 56 | m[2 * 4 + 1] = 0.0; 57 | m[2 * 4 + 2] = (zFar / (zNear - zFar)) * -handednessScale; 58 | m[2 * 4 + 3] = (zFar * zNear) / (zNear - zFar); 59 | m[3 * 4 + 0] = 0.0; 60 | m[3 * 4 + 1] = 0.0; 61 | m[3 * 4 + 2] = handednessScale; 62 | m[3 * 4 + 3] = 0.0; 63 | matrix.transpose(); 64 | } 65 | 66 | FovToProjection( 67 | matrix, 68 | fov, 69 | rightHanded /* = true */, 70 | zNear /* = 0.01 */, 71 | zFar /* = 10000.0 */ 72 | ) { 73 | const fovPort = { 74 | upTan: Math.tan((fov.upDegrees * Math.PI) / 180.0), 75 | downTan: Math.tan((fov.downDegrees * Math.PI) / 180.0), 76 | leftTan: Math.tan((fov.leftDegrees * Math.PI) / 180.0), 77 | rightTan: Math.tan((fov.rightDegrees * Math.PI) / 180.0), 78 | }; 79 | return this.FovPortToProjection(matrix, fovPort, rightHanded, zNear, zFar); 80 | } 81 | 82 | render(scene, camera) { 83 | this.FovToProjection( 84 | this.cameraLeft.projectionMatrix, 85 | this.fovLeft, 86 | true, 87 | camera.near, 88 | camera.far 89 | ); 90 | this.FovToProjection( 91 | this.cameraRight.projectionMatrix, 92 | this.fovRight, 93 | true, 94 | camera.near, 95 | camera.far 96 | ); 97 | 98 | this.right.set(this.halfIPD, 0, 0); 99 | this.right.applyQuaternion(camera.quaternion); 100 | 101 | this.cameraLeft.position.copy(camera.position).sub(this.right); 102 | this.cameraRight.position.copy(camera.position).add(this.right); 103 | 104 | this.cameraLeft.quaternion.copy(camera.quaternion); 105 | this.cameraRight.quaternion.copy(camera.quaternion); 106 | 107 | const dpr = this.renderer.devicePixelRatio || 1; 108 | const width = this.renderer.domElement.width / 2 / dpr; 109 | const height = this.renderer.domElement.height / dpr; 110 | 111 | this.renderer.enableScissorTest(true); 112 | 113 | this.renderer.setViewport(0, 0, width, height); 114 | this.renderer.setScissor(0, 0, width, height); 115 | this.renderer.render(scene, this.cameraLeft); 116 | 117 | this.renderer.setViewport(width, 0, width, height); 118 | this.renderer.setScissor(width, 0, width, height); 119 | this.renderer.render(scene, this.cameraRight); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/renderers/index.js: -------------------------------------------------------------------------------- 1 | import "./MultiRenderer"; 2 | import "./VRRenderer"; 3 | -------------------------------------------------------------------------------- /test/api.spec.js: -------------------------------------------------------------------------------- 1 | import * as Threestrap from "../src"; 2 | 3 | describe("api", function () { 4 | it("sends change events", function () { 5 | let captured = {}; 6 | let api; 7 | 8 | const klass = function () { 9 | api = this.api({}); 10 | }; 11 | 12 | Threestrap.Binder.apply(klass.prototype); 13 | Threestrap.Api.apply(klass.prototype); 14 | 15 | const o = new klass(); 16 | o.on("change", function (event) { 17 | captured = event.changes; 18 | expect(event.changes.foo).toBe(this.options.foo); 19 | }); 20 | 21 | api.set({ foo: "wtf" }); 22 | expect(captured.foo).toBe("wtf"); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/binder.spec.js: -------------------------------------------------------------------------------- 1 | import * as Threestrap from "../src" 2 | 3 | describe("binder", function () { 4 | it("binds/unbinds events", function () { 5 | let ready = 0; 6 | let foo = 0; 7 | let wtf = 0; 8 | 9 | const context = {}; 10 | Threestrap.Binder.apply(context); 11 | 12 | const object = { 13 | listen: ["ready", "this.foo:baz", [context, "wtf"]], 14 | ready: function (event, _context) { 15 | expect(event.type).toBe("ready"); 16 | expect(context).toBe(_context); 17 | expect(this).toBe(object); 18 | ready++; 19 | }, 20 | baz: function (event, _context) { 21 | expect(event.type).toBe("foo"); 22 | expect(context).toBe(_context); 23 | expect(this).toBe(object); 24 | foo++; 25 | }, 26 | wtf: function (event, _context) { 27 | expect(event.type).toBe("wtf"); 28 | expect(context).toBe(_context); 29 | expect(this).toBe(object); 30 | wtf++; 31 | }, 32 | }; 33 | Threestrap.Binder.apply(object); 34 | 35 | const bind = Threestrap.Binder.bind(context, {}); 36 | const unbind = Threestrap.Binder.unbind(context); 37 | 38 | object.listen.forEach(key => { 39 | bind(key, object); 40 | }) 41 | 42 | expect(ready).toBe(0); 43 | expect(foo).toBe(0); 44 | expect(wtf).toBe(0); 45 | 46 | context.trigger({ type: "ready" }); 47 | object.trigger({ type: "foo" }); 48 | context.trigger({ type: "wtf" }); 49 | 50 | expect(ready).toBe(1); 51 | expect(foo).toBe(1); 52 | expect(wtf).toBe(1); 53 | 54 | unbind(object); 55 | 56 | context.trigger({ type: "ready" }); 57 | object.trigger({ type: "foo" }); 58 | context.trigger({ type: "wtf" }); 59 | 60 | expect(ready).toBe(1); 61 | expect(foo).toBe(1); 62 | expect(wtf).toBe(1); 63 | }); 64 | 65 | it("binds/unbinds once events", function () { 66 | let ready = 0; 67 | 68 | const context = {}; 69 | Threestrap.Binder.apply(context); 70 | 71 | const object = { 72 | listen: ["ready"], 73 | ready: function (event, _context) { 74 | expect(event.type).toBe("ready"); 75 | expect(context).toBe(_context); 76 | expect(this).toBe(object); 77 | ready++; 78 | }, 79 | }; 80 | Threestrap.Binder.apply(object); 81 | 82 | const bind = Threestrap.Binder.bind(context, {}); 83 | const unbind = Threestrap.Binder.unbind(context); 84 | 85 | object.listen.forEach(key => { 86 | bind(key, object); 87 | }) 88 | 89 | expect(ready).toBe(0); 90 | 91 | context.triggerOnce({ type: "ready" }); 92 | 93 | expect(ready).toBe(1); 94 | 95 | context.triggerOnce({ type: "ready" }); 96 | 97 | expect(ready).toBe(1); 98 | 99 | unbind(object); 100 | 101 | expect(ready).toBe(1); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /test/bootstrap.spec.js: -------------------------------------------------------------------------------- 1 | import * as Threestrap from "../src"; 2 | 3 | describe("three", function () { 4 | it("initializes and destroys once", function () { 5 | const options = { 6 | init: false, 7 | plugins: [], 8 | }; 9 | 10 | const three = new Threestrap.Bootstrap(options); 11 | 12 | expect(three.__inited).toEqual(false); 13 | 14 | three.init(); 15 | 16 | expect(three.__inited).toEqual(true); 17 | expect(three.__destroyed).toEqual(false); 18 | 19 | three.destroy(); 20 | 21 | expect(three.__destroyed).toEqual(true); 22 | 23 | let called = false; 24 | three.on("ready", function () { 25 | called = true; 26 | }); 27 | three.init(); 28 | expect(called).toBe(false); 29 | }); 30 | 31 | it("autoinits", function () { 32 | const options = { 33 | init: true, 34 | plugins: [], 35 | }; 36 | 37 | const three = new Threestrap.Bootstrap(options); 38 | 39 | expect(three.__inited).toEqual(true); 40 | 41 | three.destroy(); 42 | }); 43 | 44 | it("installs in an element", function () { 45 | const element = document.createElement("div"); 46 | document.body.appendChild(element); 47 | 48 | const options = { 49 | init: true, 50 | plugins: [], 51 | element: element, 52 | }; 53 | 54 | const three = new Threestrap.Bootstrap(options); 55 | 56 | expect(three.__inited).toEqual(true); 57 | expect(three.element).toEqual(element); 58 | 59 | three.destroy(); 60 | 61 | document.body.removeChild(element); 62 | }); 63 | 64 | it("installs in an element (shorthand)", function () { 65 | const element = document.createElement("div"); 66 | document.body.appendChild(element); 67 | 68 | const options = { 69 | init: true, 70 | plugins: [], 71 | }; 72 | 73 | const three = new Threestrap.Bootstrap(element, options); 74 | 75 | expect(three.__inited).toEqual(true); 76 | expect(three.element).toEqual(element); 77 | 78 | three.destroy(); 79 | 80 | document.body.removeChild(element); 81 | }); 82 | 83 | it("installs in an element (selector)", function () { 84 | const element = document.createElement("div"); 85 | element.setAttribute("id", "watwatwatselector"); 86 | document.body.appendChild(element); 87 | 88 | const options = { 89 | init: true, 90 | plugins: [], 91 | element: "#watwatwatselector", 92 | }; 93 | 94 | const three = new Threestrap.Bootstrap(options); 95 | 96 | expect(three.__inited).toEqual(true); 97 | expect(three.element).toEqual(element); 98 | 99 | three.destroy(); 100 | 101 | document.body.removeChild(element); 102 | }); 103 | 104 | it("fires a ready event", function () { 105 | let ready = 0; 106 | 107 | const options = { 108 | init: false, 109 | plugins: [], 110 | }; 111 | 112 | const three = new Threestrap.Bootstrap(options); 113 | three.on("ready", function () { 114 | ready++; 115 | }); 116 | 117 | expect(ready).toBe(0); 118 | 119 | three.init(); 120 | 121 | expect(ready).toBe(1); 122 | 123 | three.destroy(); 124 | 125 | expect(ready).toBe(1); 126 | }); 127 | 128 | it("adds/removes handlers", function () { 129 | let update = 0; 130 | 131 | const options = { 132 | init: false, 133 | plugins: [], 134 | }; 135 | 136 | const three = new Threestrap.Bootstrap(options); 137 | let cb; 138 | three.on( 139 | "update", 140 | (cb = function () { 141 | update++; 142 | }) 143 | ); 144 | 145 | expect(update).toBe(0); 146 | 147 | three.init(); 148 | 149 | expect(update).toBe(0); 150 | three.trigger({ type: "update" }); 151 | 152 | expect(update).toBe(1); 153 | 154 | three.trigger({ type: "update" }); 155 | expect(update).toBe(2); 156 | 157 | three.off("update", cb); 158 | 159 | three.trigger({ type: "update" }); 160 | expect(update).toBe(2); 161 | 162 | three.destroy(); 163 | }); 164 | 165 | it("installs/uninstall a plugin", function () { 166 | const spec = { 167 | install: function () {}, 168 | uninstall: function () {}, 169 | bind: function () {}, 170 | unbind: function () {}, 171 | }; 172 | 173 | spyOn(spec, "install"); 174 | spyOn(spec, "uninstall"); 175 | 176 | const mock = function () {}; 177 | mock.prototype = spec; 178 | 179 | const options = { 180 | init: false, 181 | plugins: ["mock"], 182 | plugindb: { mock: mock }, 183 | aliasdb: {}, 184 | }; 185 | 186 | const three = new Threestrap.Bootstrap(options); 187 | 188 | expect(spec.install.calls.count()).toEqual(0); 189 | 190 | three.init(); 191 | 192 | expect(spec.uninstall.calls.count()).toEqual(0); 193 | expect(spec.install.calls.count()).toEqual(1); 194 | 195 | three.destroy(); 196 | 197 | expect(spec.uninstall.calls.count()).toEqual(1); 198 | }); 199 | 200 | it("installs/uninstall an aliased plugin", function () { 201 | const spec = { 202 | install: function () {}, 203 | uninstall: function () {}, 204 | bind: function () {}, 205 | unbind: function () {}, 206 | }; 207 | 208 | spyOn(spec, "install"); 209 | spyOn(spec, "uninstall"); 210 | 211 | const mock = function () {}; 212 | mock.prototype = spec; 213 | 214 | const options = { 215 | init: false, 216 | aliases: { core: ["mock"] }, 217 | plugins: ["core", "mock:mock2"], 218 | plugindb: { mock2: mock }, 219 | aliasdb: {}, 220 | }; 221 | 222 | const three = new Threestrap.Bootstrap(options); 223 | 224 | expect(spec.install.calls.count()).toEqual(0); 225 | 226 | three.init(); 227 | 228 | expect(spec.uninstall.calls.count()).toEqual(0); 229 | expect(spec.install.calls.count()).toEqual(1); 230 | 231 | three.destroy(); 232 | 233 | expect(spec.uninstall.calls.count()).toEqual(1); 234 | }); 235 | 236 | it("hot swaps a plugin", function () { 237 | let ready = false; 238 | const spec = { 239 | install: function (three) { 240 | three.on("ready", function () { 241 | ready = true; 242 | }); 243 | }, 244 | uninstall: function () {}, 245 | bind: function () {}, 246 | unbind: function () {}, 247 | }; 248 | 249 | spyOn(spec, "install").and.callThrough(); 250 | spyOn(spec, "uninstall"); 251 | 252 | const mock = function () {}; 253 | mock.prototype = spec; 254 | 255 | const options = { 256 | plugins: [], 257 | plugindb: { mock: mock }, 258 | aliasdb: {}, 259 | }; 260 | 261 | const three = new Threestrap.Bootstrap(options); 262 | 263 | expect(spec.install.calls.count()).toEqual(0); 264 | expect(ready).toBe(false); 265 | 266 | three.install("mock"); 267 | 268 | expect(spec.uninstall.calls.count()).toEqual(0); 269 | expect(spec.install.calls.count()).toEqual(1); 270 | expect(ready).toBe(true); 271 | 272 | three.uninstall("mock"); 273 | 274 | expect(spec.uninstall.calls.count()).toEqual(1); 275 | 276 | three.destroy(); 277 | }); 278 | 279 | it("expands aliases recursively", function () { 280 | const installed = [0, 0, 0, 0]; 281 | const spec = function (key) { 282 | return { 283 | install: function () { 284 | installed[key]++; 285 | }, 286 | uninstall: function () {}, 287 | bind: function () {}, 288 | unbind: function () {}, 289 | }; 290 | }; 291 | 292 | const mock1 = function () {}; 293 | const mock2 = function () {}; 294 | const mock3 = function () {}; 295 | const mock4 = function () {}; 296 | 297 | mock1.prototype = spec(0); 298 | mock2.prototype = spec(1); 299 | mock3.prototype = spec(2); 300 | mock4.prototype = spec(3); 301 | 302 | const options = { 303 | plugins: ["foo", "bar"], 304 | plugindb: { mock1: mock1, mock2: mock2, mock3: mock3, mock4: mock4 }, 305 | aliasdb: { 306 | foo: "mock1", 307 | bar: ["mock2", "baz"], 308 | baz: ["mock3", "mock4"], 309 | }, 310 | }; 311 | 312 | const three = new Threestrap.Bootstrap(options); 313 | 314 | expect(installed[0]).toEqual(1); 315 | expect(installed[1]).toEqual(1); 316 | expect(installed[2]).toEqual(1); 317 | expect(installed[3]).toEqual(1); 318 | 319 | three.destroy(); 320 | }); 321 | 322 | it("doesn't allow circular aliases", function () { 323 | const options = { 324 | plugins: ["foo"], 325 | plugindb: {}, 326 | aliasdb: { 327 | foo: ["bar"], 328 | bar: ["foo"], 329 | }, 330 | }; 331 | 332 | let caught = false; 333 | try { 334 | new Threestrap.Bootstrap(options); 335 | } catch (e) { 336 | caught = true; 337 | } 338 | 339 | expect(caught).toBe(true); 340 | }); 341 | 342 | it("expands custom aliases", function () { 343 | const installed = [0, 0, 0, 0]; 344 | const spec = function (key) { 345 | return { 346 | install: function () { 347 | installed[key]++; 348 | }, 349 | uninstall: function () {}, 350 | bind: function () {}, 351 | unbind: function () {}, 352 | }; 353 | }; 354 | 355 | const mock1 = function () {}; 356 | const mock2 = function () {}; 357 | const mock3 = function () {}; 358 | const mock4 = function () {}; 359 | 360 | mock1.prototype = spec(0); 361 | mock2.prototype = spec(1); 362 | mock3.prototype = spec(2); 363 | mock4.prototype = spec(3); 364 | 365 | const options = { 366 | plugins: ["foo", "bar"], 367 | plugindb: { mock1: mock1, mock2: mock2, mock3: mock3, mock4: mock4 }, 368 | aliases: { 369 | foo: "mock1", 370 | baz: ["mock3", "mock4"], 371 | }, 372 | aliasdb: { 373 | bar: ["mock2", "baz"], 374 | }, 375 | }; 376 | 377 | const three = new Threestrap.Bootstrap(options); 378 | 379 | expect(installed[0]).toEqual(1); 380 | expect(installed[1]).toEqual(1); 381 | expect(installed[2]).toEqual(1); 382 | expect(installed[3]).toEqual(1); 383 | 384 | three.destroy(); 385 | }); 386 | 387 | it("passed on plugin options", function () { 388 | let captured = false; 389 | 390 | const spec = { 391 | install: function () {}, 392 | uninstall: function () {}, 393 | bind: function () {}, 394 | unbind: function () {}, 395 | }; 396 | 397 | const mock = function (options) { 398 | captured = options; 399 | }; 400 | mock.prototype = spec; 401 | 402 | const options = { 403 | init: false, 404 | mock: { 405 | foo: "bar", 406 | }, 407 | plugins: ["mock"], 408 | plugindb: { mock: mock }, 409 | aliasdb: {}, 410 | }; 411 | 412 | const three = new Threestrap.Bootstrap(options); 413 | 414 | three.init(); 415 | 416 | expect(captured.foo).toBe("bar"); 417 | 418 | three.destroy(); 419 | }); 420 | 421 | it("autoinits core", function () { 422 | const three = new Threestrap.Bootstrap(); 423 | 424 | expect(three.__inited).toEqual(true); 425 | 426 | three.destroy(); 427 | }); 428 | }); 429 | -------------------------------------------------------------------------------- /test/core/bind.spec.js: -------------------------------------------------------------------------------- 1 | import * as Threestrap from "../../src" 2 | 3 | describe("bind", function () { 4 | it("binds events", function () { 5 | let ready = false; 6 | let foo = false; 7 | let wtf = false; 8 | let api; 9 | 10 | const object = {}; 11 | Threestrap.Binder.apply(object); 12 | 13 | const spec = { 14 | listen: ["ready", "this.foo:baz", [object, "wtf"]], 15 | ready: function (event, three) { 16 | expect(event.type).toBe("ready"); 17 | expect(three instanceof Threestrap.Bootstrap).toBe(true); 18 | expect(this instanceof Threestrap.Bootstrap.Plugins.mockb).toBe(true); 19 | ready = true; 20 | }, 21 | baz: function (event, three) { 22 | expect(event.type).toBe("foo"); 23 | expect(three instanceof Threestrap.Bootstrap).toBe(true); 24 | expect(this instanceof Threestrap.Bootstrap.Plugins.mockb).toBe(true); 25 | foo = true; 26 | }, 27 | wtf: function (event, three) { 28 | expect(event.type).toBe("wtf"); 29 | expect(three instanceof Threestrap.Bootstrap).toBe(true); 30 | expect(this instanceof Threestrap.Bootstrap.Plugins.mockb).toBe(true); 31 | wtf = true; 32 | }, 33 | }; 34 | 35 | Threestrap.Bootstrap.registerPlugin("mockb", spec); 36 | 37 | const options = { 38 | plugins: ["bind", "mockb"], 39 | }; 40 | 41 | const three = new Threestrap.Bootstrap(options); 42 | 43 | expect(three.bind).toBeTruthy(); 44 | expect(three.unbind).toBeTruthy(); 45 | 46 | three.plugins.mockb.trigger({ type: "foo" }); 47 | object.trigger({ type: "wtf" }); 48 | 49 | expect(ready).toBe(true); 50 | expect(foo).toBe(true); 51 | expect(wtf).toBe(true); 52 | 53 | three.destroy(); 54 | 55 | expect(three.bind).toBeFalsy(); 56 | expect(three.unbind).toBeFalsy(); 57 | 58 | Threestrap.Bootstrap.unregisterPlugin("mockb", spec); 59 | }); 60 | 61 | it("fires ready events for hot install", function () { 62 | let ready = false; 63 | let api; 64 | 65 | const object = {}; 66 | Threestrap.Binder.apply(object); 67 | 68 | const spec = { 69 | listen: ["ready"], 70 | ready: function (event, three) { 71 | expect(event.type).toBe("ready"); 72 | expect(three instanceof Threestrap.Bootstrap).toBe(true); 73 | expect(this instanceof Threestrap.Bootstrap.Plugins.mockc).toBe(true); 74 | ready = true; 75 | }, 76 | }; 77 | 78 | Threestrap.Bootstrap.registerPlugin("mockc", spec); 79 | 80 | const options = { 81 | plugins: ["bind"], 82 | }; 83 | 84 | const three = new Threestrap.Bootstrap(options); 85 | 86 | expect(three.plugins.mockc).toBeFalsy(); 87 | 88 | three.install("mockc"); 89 | 90 | expect(ready).toBe(true); 91 | 92 | three.destroy(); 93 | 94 | Threestrap.Bootstrap.unregisterPlugin("mockc", spec); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /test/core/camera.spec.js: -------------------------------------------------------------------------------- 1 | import * as Threestrap from "../../src"; 2 | import { OrthographicCamera, PerspectiveCamera } from "three"; 3 | 4 | describe("camera", function () { 5 | it("installs a perspective camera", function () { 6 | const options = { 7 | plugins: ["camera"], 8 | camera: { 9 | fov: 42, 10 | near: 1, 11 | far: 2, 12 | }, 13 | }; 14 | 15 | const three = new Threestrap.Bootstrap(options); 16 | 17 | expect(three.camera instanceof PerspectiveCamera).toBeTruthy(); 18 | expect(three.camera.fov).toEqual(42); 19 | expect(three.camera.near).toEqual(1); 20 | expect(three.camera.far).toEqual(2); 21 | 22 | three.destroy(); 23 | }); 24 | 25 | it("installs an orthographic camera", function () { 26 | const options = { 27 | plugins: ["camera"], 28 | camera: { 29 | type: "orthographic", 30 | left: 0, 31 | right: 1, 32 | top: 2, 33 | bottom: 3, 34 | near: 4, 35 | far: 5, 36 | }, 37 | }; 38 | 39 | const three = new Threestrap.Bootstrap(options); 40 | 41 | expect(three.camera instanceof OrthographicCamera).toBeTruthy(); 42 | expect(three.camera.left).toEqual(0); 43 | expect(three.camera.right).toEqual(1); 44 | expect(three.camera.top).toEqual(2); 45 | expect(three.camera.bottom).toEqual(3); 46 | expect(three.camera.near).toEqual(4); 47 | expect(three.camera.far).toEqual(5); 48 | 49 | three.destroy(); 50 | }); 51 | 52 | it("installs a custom camera", function () { 53 | let captured = null; 54 | 55 | const klass = function (parameters) { 56 | captured = parameters.foo; 57 | this.left = -1; 58 | this.right = 0; 59 | this.top = 0; 60 | this.bottom = 0; 61 | this.near = 0; 62 | this.far = 0; 63 | }; 64 | klass.prototype = new OrthographicCamera(); 65 | 66 | const options = { 67 | plugins: ["camera"], 68 | camera: { 69 | klass: klass, 70 | parameters: { foo: "bar" }, 71 | left: 0, 72 | right: 1, 73 | top: 2, 74 | bottom: 3, 75 | near: 4, 76 | far: 5, 77 | }, 78 | }; 79 | 80 | const three = new Threestrap.Bootstrap(options); 81 | 82 | expect(captured).toBe("bar"); 83 | 84 | expect(three.camera instanceof klass).toBeTruthy(); 85 | expect(three.camera.left).toEqual(0); 86 | expect(three.camera.right).toEqual(1); 87 | expect(three.camera.top).toEqual(2); 88 | expect(three.camera.bottom).toEqual(3); 89 | expect(three.camera.near).toEqual(4); 90 | expect(three.camera.far).toEqual(5); 91 | 92 | three.destroy(); 93 | }); 94 | 95 | it("sets the aspect ratio when resizing", function () { 96 | const element = document.createElement("div"); 97 | element.style.width = "12px"; 98 | element.style.height = "8px"; 99 | document.body.appendChild(element); 100 | 101 | const options = { 102 | element: element, 103 | plugins: ["bind", "renderer", "size", "camera"], 104 | }; 105 | 106 | const three = new Threestrap.Bootstrap(options); 107 | 108 | expect(three.camera.aspect).toBe(1.5); 109 | 110 | three.destroy(); 111 | 112 | document.body.removeChild(element); 113 | }); 114 | 115 | it("recreates the camera when needed", function () { 116 | const options = { 117 | plugins: ["bind", "camera"], 118 | camera: { 119 | type: "orthographic", 120 | left: 0, 121 | right: 1, 122 | top: 2, 123 | bottom: 3, 124 | near: 4, 125 | far: 5, 126 | }, 127 | }; 128 | 129 | const three = new Threestrap.Bootstrap(options); 130 | 131 | expect(three.camera instanceof OrthographicCamera).toBeTruthy(); 132 | expect(three.camera.left).toEqual(0); 133 | expect(three.camera.right).toEqual(1); 134 | expect(three.camera.top).toEqual(2); 135 | expect(three.camera.bottom).toEqual(3); 136 | expect(three.camera.near).toEqual(4); 137 | expect(three.camera.far).toEqual(5); 138 | 139 | const old = three.camera; 140 | three.Camera.set({ 141 | near: -5, 142 | far: 5, 143 | }); 144 | expect(three.camera).toEqual(old); 145 | expect(three.camera.near).toEqual(-5); 146 | expect(three.camera.far).toEqual(5); 147 | 148 | three.Camera.set({ 149 | type: "perspective", 150 | fov: 42, 151 | near: 1, 152 | far: 2, 153 | }); 154 | 155 | expect(three.camera instanceof PerspectiveCamera).toBeTruthy(); 156 | expect(three.camera.fov).toEqual(42); 157 | expect(three.camera.near).toEqual(1); 158 | expect(three.camera.far).toEqual(2); 159 | 160 | three.destroy(); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /test/core/fallback.spec.js: -------------------------------------------------------------------------------- 1 | import * as Threestrap from "../../src"; 2 | 3 | describe("fallback", function () { 4 | it("displays a fallback, halts install, and cleans up on uninstall", function () { 5 | const options = { 6 | plugins: ["fallback", "renderer"], 7 | fallback: { 8 | force: true, 9 | begin: '
', 10 | message: 'wat', 11 | end: "
", 12 | }, 13 | }; 14 | 15 | const getNode = function () { 16 | return document.querySelector(".threestrap-wat"); 17 | }; 18 | const getSpan = function () { 19 | return document.querySelector(".threestrap-wat span.wat"); 20 | }; 21 | 22 | expect(getNode()).toBe(null); 23 | expect(getSpan()).toBe(null); 24 | 25 | const three = new Threestrap.Bootstrap(options); 26 | 27 | const node = getNode(); 28 | expect(node).toBeTruthy(); 29 | expect(getSpan()).toBeTruthy(); 30 | 31 | expect(three.renderer).toBeFalsy(); 32 | expect(three.fallback).toBe(true); 33 | 34 | three.destroy(); 35 | 36 | expect(getNode()).toBe(null); 37 | expect(getSpan()).toBe(null); 38 | }); 39 | 40 | it("installs the fill plugin on failure", function () { 41 | const options = { 42 | plugins: ["fallback", "renderer"], 43 | fallback: { force: true }, 44 | }; 45 | 46 | const three = new Threestrap.Bootstrap(options); 47 | 48 | expect(three.plugins.fill).toBeTruthy(); 49 | expect(three.renderer).toBeFalsy(); 50 | 51 | three.destroy(); 52 | }); 53 | 54 | it("doesn't interfere", function () { 55 | const options = { 56 | plugins: ["fallback", "renderer"], 57 | }; 58 | 59 | const three = new Threestrap.Bootstrap(options); 60 | 61 | expect(three.fallback).toBe(false); 62 | 63 | three.destroy(); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/core/fill.spec.js: -------------------------------------------------------------------------------- 1 | import * as Threestrap from "../../src"; 2 | 3 | describe("fill", function () { 4 | it("sets/unsets html, body height", function () { 5 | function test() { 6 | return ( 7 | document.body.style.height == "100%" && 8 | document.documentElement.style.height == "100%" 9 | ); 10 | } 11 | 12 | const options = { 13 | plugins: ["fill"], 14 | }; 15 | 16 | expect(test()).toBe(false); 17 | 18 | const three = new Threestrap.Bootstrap(options); 19 | 20 | expect(test()).toBe(true); 21 | 22 | three.destroy(); 23 | 24 | expect(test()).toBe(false); 25 | }); 26 | 27 | it("makes the canvas a block element", function () { 28 | function test() { 29 | const canvas = document.querySelector("canvas"); 30 | return canvas && canvas.style.display == "block"; 31 | } 32 | 33 | const options = { 34 | plugins: ["renderer", "fill"], 35 | }; 36 | 37 | expect(test()).toBeFalsy(); 38 | 39 | const three = new Threestrap.Bootstrap(options); 40 | 41 | expect(test()).toBe(true); 42 | 43 | three.destroy(); 44 | 45 | expect(test()).toBeFalsy(); 46 | }); 47 | 48 | it("makes the containing element have layout", function () { 49 | function test() { 50 | const canvas = document.querySelector("canvas"); 51 | return canvas && canvas.parentNode.style.position == "relative"; 52 | } 53 | 54 | const element = document.createElement("div"); 55 | document.body.appendChild(element); 56 | 57 | const options = { 58 | plugins: ["renderer", "fill"], 59 | element: element, 60 | }; 61 | 62 | expect(test()).toBeFalsy(); 63 | 64 | const three = new Threestrap.Bootstrap(options); 65 | 66 | expect(test()).toBe(true); 67 | 68 | three.destroy(); 69 | 70 | expect(test()).toBeFalsy(); 71 | 72 | document.body.removeChild(element); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/core/loop.spec.js: -------------------------------------------------------------------------------- 1 | import * as Threestrap from "../../src"; 2 | 3 | const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); 4 | 5 | describe("loop", function () { 6 | it("installs start/stop methods", function () { 7 | const options = { 8 | plugins: ["loop"], 9 | loop: { 10 | start: false, 11 | }, 12 | }; 13 | 14 | const three = new Threestrap.Bootstrap(options); 15 | 16 | expect(three.Loop.start.call).toBeTruthy(); 17 | expect(three.Loop.stop.call).toBeTruthy(); 18 | 19 | three.destroy(); 20 | }); 21 | 22 | it("starts and stops", function () { 23 | const options = { 24 | plugins: ["loop"], 25 | loop: { 26 | start: false, 27 | }, 28 | }; 29 | 30 | const three = new Threestrap.Bootstrap(options); 31 | 32 | let started = false; 33 | let stopped = false; 34 | 35 | three.on("start", function () { 36 | started = true; 37 | }); 38 | 39 | three.on("stop", function () { 40 | stopped = true; 41 | }); 42 | 43 | expect(three.Loop.running).toBe(false); 44 | 45 | three.Loop.start(); 46 | 47 | expect(three.Loop.running).toBe(true); 48 | 49 | three.Loop.stop(); 50 | 51 | expect(three.Loop.running).toBe(false); 52 | 53 | three.Loop.start(); 54 | 55 | expect(three.Loop.running).toBe(true); 56 | 57 | three.Loop.stop(); 58 | 59 | expect(three.Loop.running).toBe(false); 60 | 61 | expect(started).toBe(true); 62 | expect(stopped).toBe(true); 63 | 64 | three.destroy(); 65 | }); 66 | 67 | it("loops correctly", async () => { 68 | const callOrder = [] 69 | 70 | const options = { 71 | init: false, 72 | plugins: ["bind", "loop"], 73 | }; 74 | 75 | const three = new Threestrap.Bootstrap(options); 76 | 77 | three.on("pre", () => { 78 | callOrder.push("pre") 79 | }); 80 | three.on("update", () => { 81 | callOrder.push("update") 82 | }); 83 | three.on("render", () => { 84 | callOrder.push("render") 85 | }); 86 | three.on("post", () => { 87 | callOrder.push("post") 88 | // Lets do two loop iterations, just for fun 89 | if (callOrder.length > 4) { 90 | three.Loop.stop(); 91 | } 92 | }); 93 | 94 | three.init(); 95 | 96 | await sleep(40) 97 | expect(callOrder).toEqual(["pre", "update", "render", "post", "pre", "update", "render", "post"]) 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /test/core/render.spec.js: -------------------------------------------------------------------------------- 1 | import * as Threestrap from "../../src"; 2 | import { Scene, PerspectiveCamera } from "three"; 3 | 4 | describe("render", function () { 5 | it("renders the scene on update", function () { 6 | const options = { 7 | plugins: ["bind", "renderer", "render"], 8 | }; 9 | 10 | const three = new Threestrap.Bootstrap(options); 11 | 12 | three.scene = new Scene(); 13 | three.camera = new PerspectiveCamera(); 14 | 15 | let called = 0; 16 | three.renderer.render = function () { 17 | called++; 18 | }; 19 | 20 | three.dispatchEvent({ type: "render" }); 21 | 22 | expect(called).toBe(1); 23 | 24 | three.destroy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/core/renderer.spec.js: -------------------------------------------------------------------------------- 1 | import * as Threestrap from "../../src"; 2 | import { WebGL1Renderer } from "three"; 3 | 4 | describe("renderer", function () { 5 | it("installs the canvas into the body", function () { 6 | const options = { 7 | init: false, 8 | plugins: ["renderer"], 9 | }; 10 | 11 | const three = new Threestrap.Bootstrap(options); 12 | 13 | expect(document.querySelectorAll("canvas").length).toBe(0); 14 | 15 | three.init(); 16 | 17 | expect(document.querySelectorAll("canvas").length).toBe(1); 18 | 19 | expect(three.renderer).toEqual(jasmine.any(WebGL1Renderer)); 20 | expect(three.renderer.domElement.parentNode).toEqual(document.body); 21 | 22 | three.destroy(); 23 | 24 | expect(document.querySelectorAll("canvas").length).toBe(0); 25 | }); 26 | 27 | it("installs the canvas into an element", function () { 28 | const element = document.createElement("div"); 29 | document.body.appendChild(element); 30 | 31 | const options = { 32 | init: false, 33 | element: element, 34 | plugins: ["renderer"], 35 | }; 36 | 37 | const three = new Threestrap.Bootstrap(options); 38 | 39 | expect(document.querySelectorAll("canvas").length).toBe(0); 40 | 41 | three.init(); 42 | 43 | expect(document.querySelectorAll("canvas").length).toBe(1); 44 | 45 | expect(three.renderer).toEqual(jasmine.any(WebGL1Renderer)); 46 | expect(three.renderer.domElement.parentNode).toEqual(element); 47 | 48 | three.destroy(); 49 | 50 | expect(document.querySelectorAll("canvas").length).toBe(0); 51 | 52 | document.body.removeChild(element); 53 | }); 54 | 55 | it("calls renderer setSize", function () { 56 | const element = document.createElement("div"); 57 | document.body.appendChild(element); 58 | 59 | const options = { 60 | init: false, 61 | element: element, 62 | plugins: ["renderer"], 63 | }; 64 | 65 | const three = new Threestrap.Bootstrap(options); 66 | 67 | let called = 0; 68 | const callback = function () { 69 | called++; 70 | }; 71 | 72 | three.init(); 73 | three.renderer.setSize = callback; 74 | three.plugins.renderer.resize( 75 | { 76 | renderWidth: 5, 77 | renderHeight: 4, 78 | viewWidth: 3, 79 | viewHeight: 2, 80 | aspect: 3 / 2, 81 | }, 82 | three 83 | ); 84 | three.destroy(); 85 | 86 | expect(called).toBe(1); 87 | }); 88 | 89 | it("calls renderer setSize and setRenderSize", function () { 90 | const element = document.createElement("div"); 91 | document.body.appendChild(element); 92 | 93 | const options = { 94 | init: false, 95 | element: element, 96 | plugins: ["renderer"], 97 | }; 98 | 99 | const three = new Threestrap.Bootstrap(options); 100 | 101 | let called = 0; 102 | const callback = function () { 103 | called++; 104 | }; 105 | 106 | three.init(); 107 | const el = three.renderer.domElement; 108 | three.renderer.domElement = document.createElement("div"); 109 | three.renderer.setSize = callback; 110 | three.renderer.setRenderSize = callback; 111 | three.plugins.renderer.resize( 112 | { 113 | renderWidth: 5, 114 | renderHeight: 4, 115 | viewWidth: 3, 116 | viewHeight: 2, 117 | aspect: 3 / 2, 118 | }, 119 | three 120 | ); 121 | three.renderer.domElement = el; 122 | three.destroy(); 123 | 124 | expect(called).toBe(2); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /test/core/scene.spec.js: -------------------------------------------------------------------------------- 1 | import * as Threestrap from "../../src"; 2 | import { Scene } from "three"; 3 | 4 | describe("scene", function () { 5 | it("makes a scene", function () { 6 | const options = { 7 | plugins: ["scene"], 8 | }; 9 | 10 | const three = new Threestrap.Bootstrap(options); 11 | 12 | expect(three.scene instanceof Scene).toBe(true); 13 | 14 | three.destroy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /test/core/size.spec.js: -------------------------------------------------------------------------------- 1 | import * as Threestrap from "../../src"; 2 | 3 | describe("size", function () { 4 | it("fits the canvas in an element", function () { 5 | const element = document.createElement("div"); 6 | element.style.width = "451px"; 7 | element.style.height = "251px"; 8 | document.body.appendChild(element); 9 | 10 | const options = { 11 | element: element, 12 | plugins: ["bind", "renderer", "size"], 13 | size: { 14 | devicePixelRatio: false, 15 | }, 16 | }; 17 | 18 | const three = new Threestrap.Bootstrap(options); 19 | 20 | const canvas = three.renderer.domElement; 21 | 22 | expect(canvas.width).toBe(451); 23 | expect(canvas.height).toBe(251); 24 | 25 | three.destroy(); 26 | 27 | document.body.removeChild(element); 28 | }); 29 | 30 | it("applies width, height, scale", function () { 31 | const options = { 32 | init: false, 33 | size: { 34 | width: 230, 35 | height: 130, 36 | scale: 1 / 2, 37 | devicePixelRatio: false, 38 | }, 39 | plugins: ["bind", "renderer", "size"], 40 | }; 41 | 42 | const three = new Threestrap.Bootstrap(options); 43 | 44 | let h; 45 | three.on( 46 | "resize", 47 | (h = function (event) { 48 | expect(event.pixelRatio).toBe(event.renderHeight / event.viewHeight); 49 | 50 | expect(event.viewWidth).toBe(options.size.width); 51 | expect(event.viewHeight).toBe(options.size.height); 52 | 53 | expect(event.renderWidth).toBe(options.size.width * options.size.scale); 54 | expect(event.renderHeight).toBe( 55 | options.size.height * options.size.scale 56 | ); 57 | }) 58 | ); 59 | 60 | three.init(); 61 | 62 | three.destroy(); 63 | }); 64 | 65 | it("applies devicepixelratio", function () { 66 | const options = { 67 | init: false, 68 | plugins: ["bind", "renderer", "size"], 69 | size: { 70 | width: 300, 71 | height: 200, 72 | devicePixelRatio: true, 73 | }, 74 | }; 75 | 76 | const dpr = window.devicePixelRatio; 77 | window.devicePixelRatio = 2; 78 | 79 | const three = new Threestrap.Bootstrap(options); 80 | 81 | three.on("resize", function (event) { 82 | expect(event.renderWidth).toBe(600); 83 | expect(event.renderHeight).toBe(400); 84 | expect(event.viewWidth).toBe(300); 85 | expect(event.viewHeight).toBe(200); 86 | }); 87 | 88 | three.init(); 89 | 90 | three.destroy(); 91 | 92 | window.devicePixelRatio = dpr; 93 | }); 94 | 95 | it("caps resolution while retaining aspect tall", function () { 96 | const options = { 97 | init: false, 98 | plugins: ["bind", "renderer", "size"], 99 | size: { 100 | width: 400, 101 | height: 600, 102 | maxRenderWidth: 300, 103 | maxRenderHeight: 300, 104 | devicePixelRatio: false, 105 | }, 106 | }; 107 | 108 | const three = new Threestrap.Bootstrap(options); 109 | 110 | three.on("resize", function (event) { 111 | expect(event.renderWidth).toBe(200); 112 | expect(event.renderHeight).toBe(300); 113 | }); 114 | 115 | three.init(); 116 | 117 | three.destroy(); 118 | }); 119 | 120 | it("applies width, height, scale, aspect wide", function () { 121 | const options = { 122 | init: false, 123 | size: { 124 | width: 500, 125 | height: 500, 126 | aspect: 5 / 4, 127 | scale: 1 / 2, 128 | devicePixelRatio: false, 129 | }, 130 | plugins: ["bind", "renderer", "size"], 131 | }; 132 | 133 | const three = new Threestrap.Bootstrap(options); 134 | 135 | three.on("resize", function (event) { 136 | expect(event.viewWidth).toBe(500); 137 | expect(event.viewHeight).toBe(400); 138 | 139 | expect(event.renderWidth).toBe(250); 140 | expect(event.renderHeight).toBe(200); 141 | }); 142 | 143 | three.init(); 144 | 145 | three.destroy(); 146 | }); 147 | 148 | it("applies width, height, scale, aspect tall", function () { 149 | const options = { 150 | init: false, 151 | size: { 152 | width: 500, 153 | height: 500, 154 | aspect: 4 / 5, 155 | scale: 1 / 2, 156 | devicePixelRatio: false, 157 | }, 158 | plugins: ["bind", "renderer", "size"], 159 | }; 160 | 161 | const three = new Threestrap.Bootstrap(options); 162 | 163 | three.on("resize", function (event) { 164 | expect(event.viewWidth).toBe(400); 165 | expect(event.viewHeight).toBe(500); 166 | 167 | expect(event.renderWidth).toBe(200); 168 | expect(event.renderHeight).toBe(250); 169 | }); 170 | 171 | three.init(); 172 | 173 | three.destroy(); 174 | }); 175 | 176 | it("changes on set", function () { 177 | const element = document.createElement("div"); 178 | element.style.width = "451px"; 179 | element.style.height = "251px"; 180 | document.body.appendChild(element); 181 | 182 | let options = { 183 | element: element, 184 | plugins: ["bind", "renderer", "size"], 185 | size: { 186 | width: 300, 187 | height: 200, 188 | }, 189 | }; 190 | 191 | const three = new Threestrap.Bootstrap(options); 192 | 193 | let called = false; 194 | three.on("resize", function (event) { 195 | expect(event.viewWidth).toBe(500); 196 | expect(event.viewHeight).toBe(400); 197 | 198 | expect(event.renderWidth).toBe(125); 199 | expect(event.renderHeight).toBe(100); 200 | 201 | called = true; 202 | }); 203 | 204 | options = { 205 | width: 500, 206 | height: 500, 207 | aspect: 5 / 4, 208 | scale: 1 / 2, 209 | maxRenderWidth: 150, 210 | maxRenderHeight: 100, 211 | devicePixelRatio: false, 212 | }; 213 | 214 | three.Size.set(options); 215 | 216 | three.trigger({ type: "pre" }); 217 | 218 | expect(called).toBe(true); 219 | 220 | three.destroy(); 221 | }); 222 | }); 223 | -------------------------------------------------------------------------------- /test/core/time.spec.js: -------------------------------------------------------------------------------- 1 | import * as Threestrap from "../../src"; 2 | 3 | 4 | describe("time", function () { 5 | /** 6 | * We prefer synchronous sleep here (stalling) rather than something async 7 | * (based on setTimeout) because: 8 | * - we are performing time calculations, so we want accurate-length sleeps 9 | * - setTimeout really only guarantees waiting *at least* the given timespan 10 | */ 11 | const syncSleep = (sec) => { 12 | const start = new Date().getTime(); 13 | while (new Date().getTime() < start + sec * 1000) {/** pass */} 14 | } 15 | 16 | it("installs time values", function () { 17 | const options = { 18 | plugins: ["bind", "time"], 19 | }; 20 | 21 | const three = new Threestrap.Bootstrap(options); 22 | 23 | expect(three.Time.now !== undefined).toBeTruthy(); 24 | expect(three.Time.clock !== undefined).toBeTruthy(); 25 | expect(three.Time.step !== undefined).toBeTruthy(); 26 | expect(three.Time.frames !== undefined).toBeTruthy(); 27 | expect(three.Time.time !== undefined).toBeTruthy(); 28 | expect(three.Time.delta !== undefined).toBeTruthy(); 29 | expect(three.Time.average !== undefined).toBeTruthy(); 30 | expect(three.Time.fps !== undefined).toBeTruthy(); 31 | 32 | three.destroy(); 33 | }); 34 | 35 | it("measures delta / fps correctly", () => { 36 | 37 | const options = { 38 | plugins: ["bind", "time"], 39 | }; 40 | 41 | const three = new Threestrap.Bootstrap(options); 42 | const fps = 40; 43 | const delta = 1 / fps; 44 | const frames = 5; 45 | 46 | three.trigger({ type: "pre" }); 47 | 48 | syncSleep(delta) 49 | 50 | three.trigger({ type: "pre" }); 51 | 52 | for (let i = 0; i < frames - 1; ++i) { 53 | syncSleep(delta) 54 | three.trigger({ type: "pre" }); 55 | } 56 | 57 | expect(three.Time.now).toBeGreaterThan(0); 58 | 59 | expect(three.Time.clock).toBeGreaterThan(0); 60 | expect(three.Time.step).toBeGreaterThan(0); 61 | 62 | expect(three.Time.frames).toBe(frames); 63 | expect(three.Time.time).toBeGreaterThan(0); 64 | expect(three.Time.delta).toBeGreaterThan(0); 65 | 66 | expect(three.Time.average).toBeGreaterThan(0); 67 | expect(three.Time.fps).toBeGreaterThan(0); 68 | 69 | expect(Math.abs(three.Time.delta - delta)).toBeLessThan(0.005); 70 | expect(Math.abs(three.Time.fps - fps)).toBeLessThan(5); 71 | expect(Math.abs(1 / three.Time.average - fps)).toBeLessThan(5); 72 | 73 | three.destroy(); 74 | }); 75 | 76 | it("clock runs at half speed", () => { 77 | 78 | const RATIO = 1 / 2; 79 | 80 | const options = { 81 | plugins: ["bind", "time"], 82 | time: { speed: RATIO }, 83 | }; 84 | 85 | const three = new Threestrap.Bootstrap(options); 86 | const frames = 5; 87 | const fps = 60; 88 | const delta = 1 / fps; 89 | 90 | three.trigger({ type: "pre" }); 91 | 92 | const start = three.Time.now; 93 | 94 | for (let i = 0; i < frames; ++i) { 95 | syncSleep(delta) 96 | three.trigger({ type: "pre" }); 97 | } 98 | 99 | const nowTime = three.Time.now - start; 100 | const realTime = three.Time.time; 101 | const clockTime = three.Time.clock; 102 | 103 | expect(nowTime).toBeGreaterThan(0); 104 | expect(realTime).toBeGreaterThan(0); 105 | expect(clockTime).toBeGreaterThan(0); 106 | 107 | const ratio = clockTime / realTime / RATIO; 108 | const diff = 1.0 - Math.min(ratio, 1 / ratio); 109 | expect(diff).toBeLessThan(0.05); 110 | 111 | expect(Math.abs(1.0 / three.Time.step - fps / RATIO)).toBeLessThan(5); 112 | 113 | three.destroy(); 114 | }); 115 | 116 | it("clock waits N frames then starts from 0", () => { 117 | 118 | const delay = 5; 119 | 120 | const options = { 121 | plugins: ["bind", "time"], 122 | time: { warmup: delay }, 123 | }; 124 | 125 | const three = new Threestrap.Bootstrap(options); 126 | const frames = delay; 127 | const fps = 60; 128 | const delta = 1 / fps; 129 | 130 | three.trigger({ type: "pre" }); 131 | 132 | for (let i = 0; i < frames; ++i) { 133 | syncSleep(delta) 134 | three.trigger({ type: "pre" }); 135 | } 136 | 137 | let clockTime = three.Time.clock; 138 | 139 | expect(clockTime).toBe(0); 140 | 141 | syncSleep(delta) 142 | three.trigger({ type: "pre" }); 143 | 144 | clockTime = three.Time.clock; 145 | 146 | expect(clockTime).toBeGreaterThan(0); 147 | expect(clockTime).toBeLessThan(delta * 2); 148 | 149 | three.destroy(); 150 | }); 151 | 152 | it("clock ignores frames longer than timeout", () => { 153 | const delay = 5 / 60; 154 | 155 | const options = { 156 | plugins: ["bind", "time"], 157 | time: { timeout: delay }, 158 | }; 159 | 160 | const three = new Threestrap.Bootstrap(options); 161 | const frames = 3; 162 | const fps = 60; 163 | const delta = 1 / fps; 164 | 165 | for (let i = 0; i < frames; ++i) { 166 | syncSleep(delta) 167 | three.trigger({ type: "pre" }); 168 | } 169 | 170 | const start = three.Time.clock; 171 | 172 | syncSleep(delay); 173 | three.trigger({ type: "pre" }); 174 | 175 | let clockTime = three.Time.clock; 176 | 177 | expect(clockTime - start).toBe(0); 178 | 179 | syncSleep(delta) 180 | three.trigger({ type: "pre" }); 181 | 182 | clockTime = three.Time.clock; 183 | 184 | expect(clockTime - start).toBeGreaterThan(0); 185 | expect(clockTime - start).toBeLessThan(delta * 2); 186 | 187 | three.destroy(); 188 | }); 189 | }); 190 | -------------------------------------------------------------------------------- /test/core/warmup.spec.js: -------------------------------------------------------------------------------- 1 | import * as Threestrap from "../../src"; 2 | 3 | describe("warmup", function () { 4 | it("hides canvas", function () { 5 | const n = 3; 6 | 7 | const options = { 8 | plugins: ["bind", "renderer", "warmup"], 9 | warmup: { 10 | delay: n, 11 | }, 12 | }; 13 | 14 | const three = new Threestrap.Bootstrap(options); 15 | 16 | expect(three.renderer.domElement.style.visibility).toBe("hidden"); 17 | 18 | for (let i = 0; i < n; ++i) { 19 | three.trigger({ type: "pre" }); 20 | three.trigger({ type: "post" }); 21 | expect(three.renderer.domElement.style.visibility).toBe("hidden"); 22 | } 23 | 24 | three.trigger({ type: "pre" }); 25 | three.trigger({ type: "post" }); 26 | expect(three.renderer.domElement.style.visibility).toBe("visible"); 27 | 28 | three.destroy(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/extra/controls.spec.js: -------------------------------------------------------------------------------- 1 | import * as Threestrap from "../../src" 2 | import { PerspectiveCamera } from "three"; 3 | import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; 4 | 5 | 6 | describe("controls", function () { 7 | it("install controls", function () { 8 | let captured = false; 9 | const klass = function (object, domElement) { 10 | expect(object instanceof PerspectiveCamera).toBe(true); 11 | expect(domElement.tagName).toBe("CANVAS"); 12 | }; 13 | klass.prototype = { 14 | update: function (delta) { 15 | captured = delta > 0; 16 | }, 17 | }; 18 | 19 | const options = { 20 | plugins: ["bind", "renderer", "camera", "controls"], 21 | controls: { 22 | klass: klass, 23 | parameters: { 24 | foo: "bar", 25 | }, 26 | }, 27 | }; 28 | 29 | const three = new Threestrap.Bootstrap(options); 30 | 31 | expect(three.controls instanceof klass).toBe(true); 32 | 33 | three.dispatchEvent({ type: "update" }); 34 | 35 | expect(captured).toBe(true); 36 | 37 | expect(three.controls.foo).toBe("bar"); 38 | 39 | three.destroy(); 40 | }); 41 | 42 | it("responds to camera changes", function () { 43 | const options = { 44 | plugins: ["bind", "renderer", "camera", "controls"], 45 | controls: { 46 | klass: OrbitControls, 47 | }, 48 | }; 49 | 50 | const three = new Threestrap.Bootstrap(options); 51 | 52 | expect(three.controls.object).toBe(three.camera); 53 | 54 | three.Camera.set({ 55 | type: "orthographic", 56 | }); 57 | 58 | expect(three.controls.object).toBe(three.camera); 59 | 60 | three.destroy(); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/extra/cursor.spec.js: -------------------------------------------------------------------------------- 1 | import * as Threestrap from "../../src" 2 | import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; 3 | 4 | describe("cursor", function () { 5 | it("sets and autohides the cursor", function () { 6 | const options = { 7 | plugins: ["bind", "renderer", "camera", "cursor"], 8 | cursor: { 9 | hide: true, 10 | timeout: 1, 11 | cursor: "pointer", 12 | }, 13 | }; 14 | 15 | const three = new Threestrap.Bootstrap(options); 16 | 17 | expect(three.element.style.cursor).toBe("pointer"); 18 | 19 | three.trigger({ type: "update" }); 20 | 21 | expect(three.element.style.cursor).toBe("pointer"); 22 | 23 | for (let i = 0; i < 65; ++i) { 24 | three.trigger({ type: "update" }); 25 | } 26 | 27 | expect(three.element.style.cursor).toBe("none"); 28 | 29 | three.plugins.cursor.mousemove({ type: "mousemove" }, three); 30 | 31 | expect(three.element.style.cursor).toBe("pointer"); 32 | 33 | for (let i = 0; i < 65; ++i) { 34 | three.trigger({ type: "update" }); 35 | } 36 | 37 | expect(three.element.style.cursor).toBe("none"); 38 | 39 | three.destroy(); 40 | }); 41 | 42 | it("sets the cursor contextually", function () { 43 | const options = { 44 | plugins: ["bind", "renderer", "camera", "controls", "cursor"], 45 | controls: { 46 | klass: OrbitControls, 47 | }, 48 | }; 49 | 50 | const three = new Threestrap.Bootstrap(options); 51 | 52 | expect(three.element.style.cursor).toBe("move"); 53 | 54 | three.trigger({ type: "update" }); 55 | 56 | expect(three.element.style.cursor).toBe("move"); 57 | 58 | three.uninstall("controls"); 59 | 60 | expect(three.element.style.cursor).toBe(""); 61 | 62 | three.install("controls"); 63 | 64 | expect(three.element.style.cursor).toBe("move"); 65 | 66 | three.destroy(); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/extra/fullscreen.spec.js: -------------------------------------------------------------------------------- 1 | import * as Threestrap from "../../src" 2 | describe("fullscreen", function () { 3 | it("adds fullscreen api", function () { 4 | const options = { 5 | plugins: ["bind", "renderer", "fullscreen"], 6 | }; 7 | 8 | const three = new Threestrap.Bootstrap(options); 9 | 10 | expect(three.Fullscreen).toBeTruthy(); 11 | expect(three.Fullscreen.toggle).toBeTruthy(); 12 | expect(three.Fullscreen.active).toBeFalsy(); 13 | 14 | three.destroy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /test/extra/stats.spec.js: -------------------------------------------------------------------------------- 1 | import * as Threestrap from "../../src" 2 | 3 | describe("stats", function () { 4 | it("adds stats to the dom", function () { 5 | const containerId = "#stats-test-container" 6 | const container = document.createElement("div") 7 | container.id = containerId 8 | document.body.appendChild(container) 9 | const cleanupDOM = () => container.remove() 10 | 11 | const options = { 12 | plugins: ["bind", "renderer", "stats"], 13 | element: container 14 | } 15 | 16 | expect(container.querySelector("div")).toBeFalsy(); 17 | 18 | const three = new Threestrap.Bootstrap(options); 19 | 20 | expect(container.querySelector("div")).toBeTruthy(); 21 | expect(container.contains(three.stats.dom)).toBe(true) 22 | 23 | three.destroy(); 24 | 25 | expect(container.querySelector("div")).toBeFalsy(); 26 | 27 | cleanupDOM() 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/extra/vr.spec.js: -------------------------------------------------------------------------------- 1 | import * as Threestrap from "../../src" 2 | 3 | describe("vr", function () { 4 | it("adds vr api", function () { 5 | const options = { 6 | plugins: ["bind", "renderer", "scene", "camera", "vr"], 7 | }; 8 | 9 | const three = new Threestrap.Bootstrap(options); 10 | 11 | // Fire window.onload 12 | three.plugins.vr.load({}, three); 13 | three.trigger({ type: "pre" }); 14 | 15 | expect(three.VR).toBeTruthy(); 16 | expect(three.VR.active).toBeTruthy(); 17 | expect(three.VR.devices).toBeTruthy(); 18 | expect(three.VR.devices.length).toBeGreaterThan(0); 19 | 20 | three.destroy(); 21 | }); 22 | 23 | it("fires vr event", function () { 24 | const options = { 25 | plugins: ["bind", "renderer", "scene", "camera", "vr"], 26 | }; 27 | 28 | const three = new Threestrap.Bootstrap(options); 29 | 30 | // Fire window.onload 31 | three.plugins.vr.load({}, three); 32 | 33 | let called = 0; 34 | const handler = function () { 35 | called++; 36 | }; 37 | three.on("vr", handler); 38 | 39 | three.trigger({ type: "pre" }); 40 | 41 | expect(called).toBe(1); 42 | 43 | three.off("vr", handler); 44 | 45 | three.destroy(); 46 | 47 | expect(called).toBe(1); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/plugin.spec.js: -------------------------------------------------------------------------------- 1 | import * as Threestrap from "../src"; 2 | 3 | describe("plugin", function () { 4 | it("registers a plugin", function () { 5 | const spec = {}; 6 | 7 | expect(Threestrap.Bootstrap.Plugins.mockp1).toBeFalsy(); 8 | 9 | Threestrap.Bootstrap.registerPlugin("mockp1", spec); 10 | 11 | expect(new Threestrap.Bootstrap.Plugins.mockp1()).toEqual( 12 | jasmine.any(Threestrap.Bootstrap.Plugin) 13 | ); 14 | 15 | Threestrap.Bootstrap.unregisterPlugin("mockp1", spec); 16 | 17 | expect(Threestrap.Bootstrap.Plugins.mockp1).toBeFalsy(); 18 | }); 19 | 20 | it("sets defaults", function () { 21 | let captured = {}; 22 | 23 | const spec = { 24 | install: function () { 25 | captured = this.options; 26 | }, 27 | defaults: { 28 | foo: "bar", 29 | foos: "bars", 30 | }, 31 | }; 32 | 33 | Threestrap.Bootstrap.registerPlugin("mockp2", spec); 34 | 35 | const options = { 36 | init: false, 37 | mockp2: { 38 | foo: "baz", 39 | }, 40 | plugins: ["mockp2"], 41 | }; 42 | 43 | const three = new Threestrap.Bootstrap(options); 44 | 45 | three.init(); 46 | 47 | expect(captured.foo).toBe("baz"); 48 | expect(captured.foos).toBe("bars"); 49 | 50 | three.destroy(); 51 | 52 | Threestrap.Bootstrap.unregisterPlugin("mockp2", spec); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Not relevant because we we don't run tsc directly, webpack does that 4 | // But without something here, tsc file shows errors. 5 | "outDir": "./not_relevant", 6 | "target": "ESNext", 7 | "allowJs": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "baseUrl": "src", 18 | "paths": { 19 | "@/*": ["./*"], 20 | }, 21 | "types": ["node"] 22 | }, 23 | "include": ["src/**/*", "./webpack.config.ts"], 24 | "exclude": ["node_modules"], 25 | "ts-node": { 26 | "compilerOptions": { 27 | "module": "CommonJS" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path" 2 | import TerserPlugin from 'terser-webpack-plugin' 3 | import type { Configuration } from "webpack" 4 | import * as glob from "glob" 5 | 6 | const LIBRARY_NAME = "Threestrap"; 7 | const PATHS = { 8 | entryPoint: path.resolve(__dirname, "src/index.js"), 9 | libraryBundles: path.resolve(__dirname, "build"), 10 | testBundle: path.resolve(__dirname, "build_tests"), 11 | testFiles: glob.sync("./test/**/*.spec.js"), 12 | }; 13 | 14 | const umdBase = (path: string): Configuration => ({ 15 | // The output defines how and where we want the bundles. The special value 16 | // `[name]` in `filename` refers to the keys of `entry`. With a UMD target, 17 | // when including the bundle in a browser, the bundle will be available as 18 | // `window.entryKeyName`. 19 | output: { 20 | path, 21 | filename: "[name].js", 22 | libraryTarget: "umd", 23 | library: LIBRARY_NAME, 24 | umdNamedDefine: true, 25 | }, 26 | resolve: { 27 | extensions: [".js"], 28 | }, 29 | // Activate source maps for the bundles in order to preserve the original 30 | // source when the user debugs the application 31 | devtool: "source-map", 32 | }); 33 | 34 | const library: Configuration = { 35 | entry: { 36 | threestrap: [PATHS.entryPoint], 37 | "threestrap.min": [PATHS.entryPoint], 38 | }, 39 | externals: { 40 | three: "THREE", 41 | }, 42 | optimization: { 43 | minimize: true, 44 | minimizer: [ 45 | new TerserPlugin({ 46 | test: /\.min\.js$/, 47 | }), 48 | ], 49 | }, 50 | } 51 | 52 | const configs: Configuration[] = [ 53 | { 54 | ...umdBase(PATHS.libraryBundles), 55 | ...library, 56 | name: "threestrap", 57 | }, 58 | { 59 | ...umdBase(PATHS.testBundle), 60 | name: "tests", 61 | mode: "development", 62 | devtool: "eval", 63 | entry: { 64 | tests: PATHS.testFiles 65 | } 66 | } 67 | ] 68 | 69 | export default configs 70 | --------------------------------------------------------------------------------