├── .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 |
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 |
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 | '",
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 |
--------------------------------------------------------------------------------