├── .gitignore ├── .npmignore ├── .nvmrc ├── .prettierrc.js ├── LICENSE ├── README.md ├── babel.config.js ├── config ├── index.js ├── rollup.main.js └── rollup.physics.js ├── dist └── ammo.js ├── examples ├── 3d_ui │ ├── index.html │ ├── index.js │ └── ui.js ├── animations │ ├── assets │ │ ├── animated.fbx │ │ ├── character_duck.fbx │ │ ├── duck_animation.fbx │ │ ├── fire.png │ │ └── skeleton_animation.fbx │ ├── index.html │ └── index.js ├── cars_physics │ ├── .DS_Store │ ├── assets │ │ ├── .DS_Store │ │ └── models │ │ │ ├── .DS_Store │ │ │ ├── buggy.gltf │ │ │ ├── model.bin │ │ │ ├── model.gltf │ │ │ └── wheel.gltf │ ├── index.html │ ├── index.js │ └── speedometer.js ├── controls_events │ ├── box_A.fbx │ ├── car_police.obj │ ├── firehydrant.gltf │ ├── index.html │ ├── index.js │ ├── materials │ │ ├── car_police.mtl │ │ ├── firehydrant.bin │ │ ├── radio.mtl │ │ └── watertower.mtl │ ├── radio.obj │ ├── simplescene.mtl │ ├── simplescene.obj │ ├── textures │ │ └── citybits_texture.png │ └── watertower.obj ├── css │ ├── .DS_Store │ ├── M.css │ ├── app.css │ └── ka1.ttf ├── deferredSound │ ├── click.ogg │ ├── fire.png │ ├── fire.wav │ ├── forest.mp3 │ ├── index.html │ ├── index.js │ ├── radio.mtl │ ├── radio.obj │ ├── radio.wav │ ├── radiotune.mp3 │ ├── simplescene.model │ ├── simplescene.mtl │ ├── simplescene.obj │ ├── sound.png │ ├── speaker-hifi-bold.png │ └── swat.mp3 ├── directionalSound │ ├── click.ogg │ ├── fire.png │ ├── fire.wav │ ├── forest.mp3 │ ├── index.html │ ├── index.js │ ├── radio.mtl │ ├── radio.obj │ ├── radio.wav │ ├── radiotune.mp3 │ ├── simplescene.model │ ├── simplescene.mtl │ ├── simplescene.obj │ └── swat.mp3 ├── hierarchy │ ├── index.html │ └── index.js ├── index.html ├── loadModel │ ├── click.ogg │ ├── fire.png │ ├── fire.wav │ ├── forest.mp3 │ ├── index.html │ ├── index.js │ ├── radio.mtl │ ├── radio.obj │ ├── radio.wav │ ├── radiotune.mp3 │ ├── simplescene.model │ ├── simplescene.mtl │ ├── simplescene.obj │ ├── sound.png │ ├── speaker-hifi-bold.png │ └── swat.mp3 ├── loadModelMaterials │ ├── box_A.fbx │ ├── car_police.obj │ ├── firehydrant.gltf │ ├── index.html │ ├── index.js │ ├── materials │ │ ├── car_police.mtl │ │ ├── firehydrant.bin │ │ ├── radio.mtl │ │ └── watertower.mtl │ ├── radio.obj │ ├── simplescene.mtl │ ├── simplescene.obj │ ├── textures │ │ └── citybits_texture.png │ └── watertower.obj ├── material_test │ ├── assets │ │ └── models │ │ │ ├── buggy.gltf │ │ │ ├── castletrack.fbx │ │ │ ├── castletrack.glb │ │ │ ├── castletrack.mtl │ │ │ ├── castletrack.obj │ │ │ ├── forestTileTower.glb │ │ │ ├── hex_forest_roadB.gltf.glb │ │ │ ├── model.bin │ │ │ ├── model.gltf │ │ │ └── wheel.gltf │ ├── index.html │ └── index.js ├── particles_custom_emitter │ ├── dot.png │ ├── dot_1.png │ ├── fire.png │ ├── green_energy.png │ ├── hex_forest_roadB.gltf.glb │ ├── index.html │ └── index.js ├── particles_explosion │ ├── .DS_Store │ ├── dot.png │ ├── dot_1.png │ ├── fire.png │ ├── index.html │ └── index.js ├── particles_fire │ ├── dot.png │ ├── dot_1.png │ ├── fire.png │ ├── index.html │ └── index.js ├── particles_sparkles │ ├── dot.png │ ├── dot_1.png │ ├── fire.png │ ├── green_energy.png │ ├── index.html │ └── index.js ├── physics │ ├── index.html │ └── index.js ├── rotateTo │ ├── assets │ │ └── models │ │ │ ├── buggy.gltf │ │ │ ├── castletrack.fbx │ │ │ ├── castletrack.glb │ │ │ ├── castletrack.mtl │ │ │ ├── castletrack.obj │ │ │ ├── forestTileTower.glb │ │ │ ├── hex_forest_roadB.gltf.glb │ │ │ ├── model.bin │ │ │ ├── model.gltf │ │ │ └── wheel.gltf │ ├── index.html │ └── index.js ├── scaleTo │ ├── assets │ │ └── models │ │ │ ├── buggy.gltf │ │ │ ├── castletrack.fbx │ │ │ ├── castletrack.glb │ │ │ ├── castletrack.mtl │ │ │ ├── castletrack.obj │ │ │ ├── forestTileTower.glb │ │ │ ├── hex_forest_roadB.gltf.glb │ │ │ ├── model.bin │ │ │ ├── model.gltf │ │ │ └── wheel.gltf │ ├── index.html │ └── index.js ├── selection_effect │ ├── index.html │ └── index.js ├── sound │ ├── click.ogg │ ├── index.html │ └── index.js ├── spotlight │ ├── assets │ │ ├── models │ │ │ ├── Textures │ │ │ │ └── roof(Clone).png │ │ │ ├── lamp.gltf │ │ │ ├── lamp.json │ │ │ ├── lamp.mtl │ │ │ └── lamp.obj │ │ └── textures │ │ │ ├── ambientlight.png │ │ │ ├── hemispherelight.png │ │ │ ├── lightbulb.png │ │ │ ├── pointlight.png │ │ │ ├── spotlight.png │ │ │ ├── sunlight.png │ │ │ └── target.png │ ├── index.html │ └── index.js ├── sunlight │ ├── assets │ │ ├── models │ │ │ ├── Textures │ │ │ │ └── roof(Clone).png │ │ │ ├── lamp.gltf │ │ │ ├── lamp.json │ │ │ ├── lamp.mtl │ │ │ └── lamp.obj │ │ └── textures │ │ │ ├── ambientlight.png │ │ │ ├── hemispherelight.png │ │ │ ├── lightbulb.png │ │ │ ├── pointlight.png │ │ │ ├── spotlight.png │ │ │ ├── sunlight.png │ │ │ └── target.png │ ├── index.html │ └── index.js ├── textures │ ├── assets │ │ ├── Wood_025_ambientOcclusion.jpg │ │ ├── Wood_025_basecolor.jpg │ │ ├── Wood_025_height.png │ │ ├── Wood_025_normal.jpg │ │ └── Wood_025_roughness.jpg │ ├── index.html │ └── index.js └── trails │ ├── index.html │ ├── index.js │ └── particle.png ├── package-lock.json ├── package.json └── src ├── .DS_Store ├── audio ├── Audio.js ├── ambientSound.js ├── directionalSound.js └── sound.js ├── controls ├── Controls.js ├── FirstPersonControl.js ├── FlyControl.js ├── Orbit.js ├── Transform.js ├── TransformGizmo.js ├── TransformPlane.js └── constants.js ├── core ├── Assets.js ├── Level.js ├── Scene.js ├── Stats.js ├── config.js ├── input │ ├── Gamepad.js │ ├── Input.js │ ├── Keyboard.js │ ├── Mouse.js │ └── constants.js ├── universe.js └── window.js ├── entities ├── Element.js ├── Entity.js ├── animations │ ├── AnimationHandler.js │ ├── SpriteAnimationHandler.js │ └── lib │ │ └── NodeFrame.js ├── base │ ├── AnimatedSprite.js │ ├── Axes.js │ ├── Box.js │ ├── Cone.js │ ├── Cube.js │ ├── CurveLine.js │ ├── Cylinder.js │ ├── Grid.js │ ├── HelperSprite.js │ ├── Label.js │ ├── Line.js │ ├── Plane.js │ ├── Sphere.js │ ├── Sprite.js │ └── index.js ├── camera.js ├── constants.js └── index.js ├── fx ├── materials │ ├── Atmosphere.js │ ├── Mirror.js │ ├── Ocean.js │ ├── OceanShaders.js │ ├── Water.js │ └── Water.old.js ├── particles │ ├── Explosion.js │ ├── Fire.js │ ├── Fountain.js │ ├── ParticleEmitter.js │ ├── ParticleEmitterGroup.js │ ├── Particles.js │ ├── ProtonParticleEmitter.js │ ├── Rain.js │ ├── Snow.js │ ├── Trail.js │ └── constants.js ├── postprocessing │ ├── .DS_Store │ ├── BloomPass.js │ ├── BokehPass.js │ ├── DepthOfField.js │ ├── GlitchEffect.js │ ├── HueSaturationEffect.js │ ├── OutlineEffect.js │ ├── PixelEffect.js │ ├── PostProcessing.js │ ├── SelectiveOutline.js │ ├── SepiaEffect.js │ ├── passes │ │ ├── ClearMaskPass.js │ │ ├── EffectComposer.js │ │ ├── MaskPass.js │ │ ├── Pass.js │ │ ├── RenderPass.js │ │ └── ShaderPass.js │ └── shaders │ │ ├── BokehShader.js │ │ ├── ConvolutionShader.js │ │ ├── CopyShader.js │ │ └── DigitalGlitchShader.js ├── scenery │ ├── Sky.js │ └── Skybox.js └── shaders │ ├── Shaders.js │ └── shader.js ├── images └── Images.js ├── index.js ├── lib ├── Color.js ├── colliders.js ├── constants.js ├── dom.js ├── easing.js ├── features.js ├── fflate.js ├── functions.js ├── location.js ├── map.js ├── math.js ├── meshUtils.js ├── messages.js ├── network.js ├── palettes.js ├── query.js ├── strings.js ├── url.js ├── utils │ └── assets.js ├── uuid.js └── workers.js ├── lights ├── HemisphereLight.js ├── Lights.js ├── SpotLight.js ├── SunLight.js ├── ambientLight.js ├── constants.js ├── csm │ ├── CascadeShadowMaps.js │ ├── Frustum.js │ └── Shader.js ├── light.js ├── pointLight.js └── utils.js ├── loaders ├── ColladaLoader.js ├── FBXLoader.js ├── GLTFLoader.js ├── Loader.js ├── MTLLoader.js ├── OBJLoader.js ├── OBJMTLLoader.js ├── RequirementsTracer.js ├── TGALoader.js ├── curves │ ├── NURBSCurve.js │ └── NURBSUtils.js └── utils.js ├── materials ├── Toon.js ├── constants.js └── helpers.js ├── models ├── Models.js └── SkeletonUtils.js ├── physics ├── constants.js ├── hitbox.js ├── index.js ├── messages.js ├── utils.js └── worker │ ├── effects.js │ ├── elements.js │ ├── index.js │ ├── lib │ ├── dispatcher.js │ └── math.js │ ├── player.js │ ├── vehicles.js │ └── world.js ├── router └── Router.js ├── runner └── GameRunner.js ├── scripts ├── BaseScript.js ├── Scripts.js ├── StaticScript.js └── builtin │ ├── BaseCar.js │ ├── SmoothCameraFollow.js │ ├── SmoothCarFollow.js │ └── Trails.js ├── storage └── storage.js ├── store ├── Store.js ├── actions │ ├── input.js │ ├── location.js │ ├── network.js │ ├── storage.js │ ├── types.js │ └── ui.js ├── index.js └── reducers │ ├── index.js │ ├── info.js │ ├── input.js │ ├── location.js │ ├── network.js │ ├── root.js │ ├── storage.js │ └── ui.js ├── ui ├── BaseUI.js ├── LabelComponent.js └── index.js └── video └── Video.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | npm-debug.log 3 | 4 | examples/**/node_modules/* 5 | 6 | dist/mage.js 7 | dist/mage.node.js 8 | 9 | .DS_Store 10 | .idea 11 | .vscode 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | examples 3 | template 4 | scripts 5 | .DS_Store 6 | config 7 | babel.config.js 8 | webpack.config.js 9 | webpack.node.js 10 | .gitignore 11 | package-lock.json 12 | !dist/mage.js 13 | !dist/mage.node.js 14 | !dist/mage.physics.js 15 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 14.19.3 -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | printWidth: 100, 5 | trailingComma: "all", 6 | arrowParens: "avoid", 7 | semi: true, 8 | tabWidth: 4, 9 | 10 | overrides: [ 11 | { 12 | files: ["*.md"], 13 | options: { printWidth: 120 }, 14 | }, 15 | { 16 | files: [".toolsharerc"], 17 | options: { parser: "yaml" }, 18 | }, 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 - 2018 by Marco Stagni and contributors. 2 | 3 | Some rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are 7 | met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | 17 | * The names of the contributors may not be used to endorse or 18 | promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | 34 | Mage contains third party software in the 'app/vendor' directory: each 35 | file/module in this directory is distributed under its original license. 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mage 2 | 3 | ![npm](https://img.shields.io/npm/v/mage-engine?color=%232ecc71) 4 | ![npm](https://img.shields.io/npm/dy/mage-engine?color=%23e67e22) 5 | ![npm bundle size](https://img.shields.io/bundlephobia/min/mage-engine) 6 | ![GitHub repo size](https://img.shields.io/github/repo-size/MageStudio/Mage) 7 | ![Discord](https://badgen.net/discord/members/NR5ZDGFG5j) 8 | 9 | --- 10 | 11 | Mage is a game engine built on top of THREEJS. It features all you need to create fully interactive 3D application that can be distributed via web, desktop or mobile. 12 | 13 | [Documentation](https://www.mage.studio/docs) 14 | 15 | ## Development 16 | The engine is under heavy development. The best way to stay up to date with changes, new features and bug fixes is to [join the Discord server](https://discord.gg/NR5ZDGFG5j). 17 | 18 | --- 19 | 20 | ## Licence 21 | 22 | Copyright (c) 2015 - 2018 by Marco Stagni and contributors. 23 | 24 | Some rights reserved. 25 | 26 | Redistribution and use in source and binary forms, with or without 27 | modification, are permitted provided that the following conditions are 28 | met: 29 | 30 | * Redistributions of source code must retain the above copyright 31 | notice, this list of conditions and the following disclaimer. 32 | 33 | * Redistributions in binary form must reproduce the above 34 | copyright notice, this list of conditions and the following 35 | disclaimer in the documentation and/or other materials provided 36 | with the distribution. 37 | 38 | * The names of the contributors may not be used to endorse or 39 | promote products derived from this software without specific 40 | prior written permission. 41 | 42 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 43 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 44 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 45 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 46 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 47 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 48 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 49 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 50 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 51 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 52 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 53 | 54 | 55 | Mage contains third party software in the 'app/vendor' directory: each 56 | file/module in this directory is distributed under its original license. 57 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | presets: [ 3 | ["@babel/preset-env", { 4 | "targets": { 5 | "browsers": ["> 0.25%", "ie >= 11"] 6 | } 7 | }], 8 | ], 9 | plugins: [ 10 | 'babel-plugin-syntax-jsx', 11 | ["babel-plugin-inferno", {"imports": true}], 12 | "@babel/plugin-proposal-class-properties", 13 | "@babel/plugin-transform-classes" 14 | ] 15 | }; 16 | 17 | module.exports = config; 18 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | import main from './rollup.main'; 2 | 3 | export default [main] -------------------------------------------------------------------------------- /config/rollup.main.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import json from '@rollup/plugin-json'; 5 | import replace from '@rollup/plugin-replace'; 6 | 7 | import { terser } from 'rollup-plugin-terser'; 8 | import webWorkerLoader from 'rollup-plugin-web-worker-loader'; 9 | 10 | export default { 11 | input: './src/index.js', 12 | output: { 13 | file: './dist/mage.js', 14 | format: 'esm', 15 | compact: true, 16 | minifyInternalExports: false, 17 | name: 'M', 18 | globals: { 19 | process: { 20 | env: { 21 | NODE_ENV: 'development' 22 | } 23 | } 24 | } 25 | }, 26 | cache: true, 27 | perf: true, 28 | plugins: [ 29 | replace({ 30 | 'process.env.NODE_ENV': JSON.stringify('production') 31 | }), 32 | resolve(), 33 | babel({ 34 | exclude: ['node_modules/**'] 35 | }), 36 | commonjs(), 37 | webWorkerLoader({ 38 | pattern: /worker:(.+)/, 39 | targetPlatform: 'browser' 40 | }), 41 | // terser(), 42 | json() 43 | ] 44 | } -------------------------------------------------------------------------------- /config/rollup.physics.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import resolve from 'rollup-plugin-node-resolve'; 3 | import commonjs from 'rollup-plugin-commonjs'; 4 | import json from '@rollup/plugin-json'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | 7 | export default { 8 | input: './src/physics.index.js', 9 | output: { 10 | file: './dist/mage.physics.js', 11 | format: 'umd', 12 | compact: true, 13 | minifyInternalExports: false, 14 | name: 'Ammo' 15 | }, 16 | cache: true, 17 | perf: true, 18 | plugins: [ 19 | resolve(), 20 | babel({ exclude: ['node_modules/**'] }), 21 | commonjs(), 22 | json() 23 | ] 24 | } -------------------------------------------------------------------------------- /examples/3d_ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | -------------------------------------------------------------------------------- /examples/3d_ui/ui.js: -------------------------------------------------------------------------------- 1 | import { LabelComponent, createElement } from '../../dist/mage.js'; 2 | 3 | class UI extends LabelComponent { 4 | 5 | constructor(props) { 6 | super(props); 7 | 8 | this.state = { test: 0 }; 9 | } 10 | 11 | componentDidMount() { 12 | super.componentDidMount(); 13 | this.setState({ test: 1 }); 14 | setInterval(() => { 15 | this.setState({ test: this.state.test + 1 }); 16 | }, 2000); 17 | } 18 | 19 | render() { 20 | const span = createElement('span', { className: 'progress-bar-fill', style: `width: ${this.state.test}%;` }); 21 | const progressbar = createElement('div', { className: 'progress-bar', children: [span, `${this.state.test}`] }); 22 | 23 | return createElement('div', { ref: this.element, className: 'wrapper', children: progressbar }); 24 | } 25 | } 26 | 27 | export default UI; -------------------------------------------------------------------------------- /examples/animations/assets/animated.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/animations/assets/animated.fbx -------------------------------------------------------------------------------- /examples/animations/assets/character_duck.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/animations/assets/character_duck.fbx -------------------------------------------------------------------------------- /examples/animations/assets/duck_animation.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/animations/assets/duck_animation.fbx -------------------------------------------------------------------------------- /examples/animations/assets/fire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/animations/assets/fire.png -------------------------------------------------------------------------------- /examples/animations/assets/skeleton_animation.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/animations/assets/skeleton_animation.fbx -------------------------------------------------------------------------------- /examples/animations/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/cars_physics/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/cars_physics/.DS_Store -------------------------------------------------------------------------------- /examples/cars_physics/assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/cars_physics/assets/.DS_Store -------------------------------------------------------------------------------- /examples/cars_physics/assets/models/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/cars_physics/assets/models/.DS_Store -------------------------------------------------------------------------------- /examples/cars_physics/assets/models/model.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/cars_physics/assets/models/model.bin -------------------------------------------------------------------------------- /examples/cars_physics/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 34 | 35 | 36 | 37 |

0

38 |
39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/cars_physics/speedometer.js: -------------------------------------------------------------------------------- 1 | import { Label, math, LabelComponent, BaseScript, rxjs, createElement } from "../../dist/mage.js"; 2 | 3 | export class SpeedometerLabel extends LabelComponent { 4 | 5 | constructor(props) { 6 | super(props); 7 | this.state = { value: 0 }; 8 | } 9 | 10 | componentDidMount() { 11 | super.componentDidMount(); 12 | const { speed } = this.props; 13 | speed 14 | .subscribe((_value) => { 15 | const value = math.clamp(_value, 0, _value); 16 | this.setState({ value: Math.floor(value) }) 17 | }); 18 | } 19 | 20 | render() { 21 | return createElement('span', { ref: this.element, className: 'speedometer', children: `${this.state.value}` }); 22 | } 23 | } 24 | 25 | export default class Speedometer extends BaseScript { 26 | 27 | constructor() { 28 | super('Speedometer'); 29 | } 30 | 31 | start(car) { 32 | this.car = car; 33 | this.speed = new rxjs.Subject(); 34 | const label = new Label({ Component: SpeedometerLabel, speed: this.speed, width: 1, height: 1 }); 35 | 36 | this.car.add(label, car.getBody(), { waitForBody: 200, waitForBodyMaxRetries: 5 }) 37 | .then(() => label.setPosition({ x: -2 })); 38 | } 39 | 40 | physicsUpdate(dt) { 41 | this.speed.next(this.car.getPhysicsState().speed) 42 | } 43 | } -------------------------------------------------------------------------------- /examples/controls_events/box_A.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/controls_events/box_A.fbx -------------------------------------------------------------------------------- /examples/controls_events/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | In order to play any sound, a basic interaction from the user is needed 18 | 19 |
20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/controls_events/materials/car_police.mtl: -------------------------------------------------------------------------------- 1 | # Blender MTL File: 'CityBits_Export.blend' 2 | # Material Count: 1 3 | 4 | newmtl citybits_texture 5 | Ns 359.999993 6 | Ka 1.000000 1.000000 1.000000 7 | Kd 1.000000 1.000000 1.000000 8 | Ks 0.500000 0.500000 0.500000 9 | Ke 0.000000 0.000000 0.000000 10 | Ni 1.450000 11 | d 1.000000 12 | illum 2 13 | map_Kd citybits_texture.png 14 | -------------------------------------------------------------------------------- /examples/controls_events/materials/firehydrant.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/controls_events/materials/firehydrant.bin -------------------------------------------------------------------------------- /examples/controls_events/materials/radio.mtl: -------------------------------------------------------------------------------- 1 | # Created by Kenney (www.kenney.nl) 2 | 3 | newmtl metalMedium(Clone) 4 | Kd 0.3686275 0.4666667 0.4666667 5 | 6 | newmtl wood(Clone) 7 | Kd 0.8962265 0.6015712 0.3931559 8 | 9 | newmtl metal(Clone) 10 | Kd 0.7406105 0.8228667 0.8396227 11 | 12 | -------------------------------------------------------------------------------- /examples/controls_events/materials/watertower.mtl: -------------------------------------------------------------------------------- 1 | # Blender MTL File: 'CityBits_Export.blend' 2 | # Material Count: 1 3 | 4 | newmtl citybits_texture 5 | Ns 359.999993 6 | Ka 1.000000 1.000000 1.000000 7 | Kd 1.000000 1.000000 1.000000 8 | Ks 0.500000 0.500000 0.500000 9 | Ke 0.000000 0.000000 0.000000 10 | Ni 1.450000 11 | d 1.000000 12 | illum 2 13 | map_Kd citybits_texture.png 14 | -------------------------------------------------------------------------------- /examples/controls_events/simplescene.mtl: -------------------------------------------------------------------------------- 1 | # Created by Kenney (www.kenney.nl) 2 | 3 | newmtl Stone(Clone) 4 | Kd 0.192 0.228 0.246 5 | 6 | newmtl foliage(Clone) 7 | Kd 0.26 0.849 0.603 8 | 9 | newmtl wood(Clone) 10 | Kd 0.896 0.602 0.393 11 | 12 | newmtl BrownDark(Clone) 13 | Kd 0.327 0.102 0.059 14 | 15 | newmtl metal(Clone) 16 | Kd 0.773 0.796 0.906 17 | 18 | newmtl rock(Clone) 19 | Kd 0.915 0.824 0.626 20 | 21 | newmtl woodDark(Clone) 22 | Kd 0.738 0.459 0.254 23 | 24 | newmtl _defaultMat(Clone) 25 | Kd 1 1 1 26 | 27 | newmtl Green(Clone) 28 | Kd 0.065 0.501 0.266 29 | 30 | newmtl GreenDark(Clone) 31 | Kd 0.021 0.257 0.267 32 | 33 | -------------------------------------------------------------------------------- /examples/controls_events/textures/citybits_texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/controls_events/textures/citybits_texture.png -------------------------------------------------------------------------------- /examples/css/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/css/.DS_Store -------------------------------------------------------------------------------- /examples/css/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #E4DDCE; 3 | } 4 | 5 | a { 6 | text-decoration: none !important; 7 | } -------------------------------------------------------------------------------- /examples/css/ka1.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/css/ka1.ttf -------------------------------------------------------------------------------- /examples/deferredSound/click.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/deferredSound/click.ogg -------------------------------------------------------------------------------- /examples/deferredSound/fire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/deferredSound/fire.png -------------------------------------------------------------------------------- /examples/deferredSound/fire.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/deferredSound/fire.wav -------------------------------------------------------------------------------- /examples/deferredSound/forest.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/deferredSound/forest.mp3 -------------------------------------------------------------------------------- /examples/deferredSound/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | In order to play any sound, a basic interaction from the user is needed 14 | 15 |
16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/deferredSound/radio.mtl: -------------------------------------------------------------------------------- 1 | # Created by Kenney (www.kenney.nl) 2 | 3 | newmtl metalMedium(Clone) 4 | Kd 0.3686275 0.4666667 0.4666667 5 | 6 | newmtl wood(Clone) 7 | Kd 0.8962265 0.6015712 0.3931559 8 | 9 | newmtl metal(Clone) 10 | Kd 0.7406105 0.8228667 0.8396227 11 | 12 | -------------------------------------------------------------------------------- /examples/deferredSound/radio.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/deferredSound/radio.wav -------------------------------------------------------------------------------- /examples/deferredSound/radiotune.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/deferredSound/radiotune.mp3 -------------------------------------------------------------------------------- /examples/deferredSound/simplescene.mtl: -------------------------------------------------------------------------------- 1 | # Created by Kenney (www.kenney.nl) 2 | 3 | newmtl Stone(Clone) 4 | Kd 0.192 0.228 0.246 5 | 6 | newmtl foliage(Clone) 7 | Kd 0.26 0.849 0.603 8 | 9 | newmtl wood(Clone) 10 | Kd 0.896 0.602 0.393 11 | 12 | newmtl BrownDark(Clone) 13 | Kd 0.327 0.102 0.059 14 | 15 | newmtl metal(Clone) 16 | Kd 0.773 0.796 0.906 17 | 18 | newmtl rock(Clone) 19 | Kd 0.915 0.824 0.626 20 | 21 | newmtl woodDark(Clone) 22 | Kd 0.738 0.459 0.254 23 | 24 | newmtl _defaultMat(Clone) 25 | Kd 1 1 1 26 | 27 | newmtl Green(Clone) 28 | Kd 0.065 0.501 0.266 29 | 30 | newmtl GreenDark(Clone) 31 | Kd 0.021 0.257 0.267 32 | 33 | -------------------------------------------------------------------------------- /examples/deferredSound/sound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/deferredSound/sound.png -------------------------------------------------------------------------------- /examples/deferredSound/speaker-hifi-bold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/deferredSound/speaker-hifi-bold.png -------------------------------------------------------------------------------- /examples/deferredSound/swat.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/deferredSound/swat.mp3 -------------------------------------------------------------------------------- /examples/directionalSound/click.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/directionalSound/click.ogg -------------------------------------------------------------------------------- /examples/directionalSound/fire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/directionalSound/fire.png -------------------------------------------------------------------------------- /examples/directionalSound/fire.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/directionalSound/fire.wav -------------------------------------------------------------------------------- /examples/directionalSound/forest.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/directionalSound/forest.mp3 -------------------------------------------------------------------------------- /examples/directionalSound/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | In order to play any sound, a basic interaction from the user is needed 14 | 15 |
16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/directionalSound/radio.mtl: -------------------------------------------------------------------------------- 1 | # Created by Kenney (www.kenney.nl) 2 | 3 | newmtl metalMedium(Clone) 4 | Kd 0.3686275 0.4666667 0.4666667 5 | 6 | newmtl wood(Clone) 7 | Kd 0.8962265 0.6015712 0.3931559 8 | 9 | newmtl metal(Clone) 10 | Kd 0.7406105 0.8228667 0.8396227 11 | 12 | -------------------------------------------------------------------------------- /examples/directionalSound/radio.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/directionalSound/radio.wav -------------------------------------------------------------------------------- /examples/directionalSound/radiotune.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/directionalSound/radiotune.mp3 -------------------------------------------------------------------------------- /examples/directionalSound/simplescene.mtl: -------------------------------------------------------------------------------- 1 | # Created by Kenney (www.kenney.nl) 2 | 3 | newmtl Stone(Clone) 4 | Kd 0.192 0.228 0.246 5 | 6 | newmtl foliage(Clone) 7 | Kd 0.26 0.849 0.603 8 | 9 | newmtl wood(Clone) 10 | Kd 0.896 0.602 0.393 11 | 12 | newmtl BrownDark(Clone) 13 | Kd 0.327 0.102 0.059 14 | 15 | newmtl metal(Clone) 16 | Kd 0.773 0.796 0.906 17 | 18 | newmtl rock(Clone) 19 | Kd 0.915 0.824 0.626 20 | 21 | newmtl woodDark(Clone) 22 | Kd 0.738 0.459 0.254 23 | 24 | newmtl _defaultMat(Clone) 25 | Kd 1 1 1 26 | 27 | newmtl Green(Clone) 28 | Kd 0.065 0.501 0.266 29 | 30 | newmtl GreenDark(Clone) 31 | Kd 0.021 0.257 0.267 32 | 33 | -------------------------------------------------------------------------------- /examples/directionalSound/swat.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/directionalSound/swat.mp3 -------------------------------------------------------------------------------- /examples/hierarchy/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/loadModel/click.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/loadModel/click.ogg -------------------------------------------------------------------------------- /examples/loadModel/fire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/loadModel/fire.png -------------------------------------------------------------------------------- /examples/loadModel/fire.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/loadModel/fire.wav -------------------------------------------------------------------------------- /examples/loadModel/forest.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/loadModel/forest.mp3 -------------------------------------------------------------------------------- /examples/loadModel/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | In order to play any sound, a basic interaction from the user is needed 14 | 15 |
16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/loadModel/radio.mtl: -------------------------------------------------------------------------------- 1 | # Created by Kenney (www.kenney.nl) 2 | 3 | newmtl metalMedium(Clone) 4 | Kd 0.3686275 0.4666667 0.4666667 5 | 6 | newmtl wood(Clone) 7 | Kd 0.8962265 0.6015712 0.3931559 8 | 9 | newmtl metal(Clone) 10 | Kd 0.7406105 0.8228667 0.8396227 11 | 12 | -------------------------------------------------------------------------------- /examples/loadModel/radio.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/loadModel/radio.wav -------------------------------------------------------------------------------- /examples/loadModel/radiotune.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/loadModel/radiotune.mp3 -------------------------------------------------------------------------------- /examples/loadModel/simplescene.mtl: -------------------------------------------------------------------------------- 1 | # Created by Kenney (www.kenney.nl) 2 | 3 | newmtl Stone(Clone) 4 | Kd 0.192 0.228 0.246 5 | 6 | newmtl foliage(Clone) 7 | Kd 0.26 0.849 0.603 8 | 9 | newmtl wood(Clone) 10 | Kd 0.896 0.602 0.393 11 | 12 | newmtl BrownDark(Clone) 13 | Kd 0.327 0.102 0.059 14 | 15 | newmtl metal(Clone) 16 | Kd 0.773 0.796 0.906 17 | 18 | newmtl rock(Clone) 19 | Kd 0.915 0.824 0.626 20 | 21 | newmtl woodDark(Clone) 22 | Kd 0.738 0.459 0.254 23 | 24 | newmtl _defaultMat(Clone) 25 | Kd 1 1 1 26 | 27 | newmtl Green(Clone) 28 | Kd 0.065 0.501 0.266 29 | 30 | newmtl GreenDark(Clone) 31 | Kd 0.021 0.257 0.267 32 | 33 | -------------------------------------------------------------------------------- /examples/loadModel/sound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/loadModel/sound.png -------------------------------------------------------------------------------- /examples/loadModel/speaker-hifi-bold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/loadModel/speaker-hifi-bold.png -------------------------------------------------------------------------------- /examples/loadModel/swat.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/loadModel/swat.mp3 -------------------------------------------------------------------------------- /examples/loadModelMaterials/box_A.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/loadModelMaterials/box_A.fbx -------------------------------------------------------------------------------- /examples/loadModelMaterials/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | In order to play any sound, a basic interaction from the user is needed 18 | 19 |
20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/loadModelMaterials/materials/car_police.mtl: -------------------------------------------------------------------------------- 1 | # Blender MTL File: 'CityBits_Export.blend' 2 | # Material Count: 1 3 | 4 | newmtl citybits_texture 5 | Ns 359.999993 6 | Ka 1.000000 1.000000 1.000000 7 | Kd 1.000000 1.000000 1.000000 8 | Ks 0.500000 0.500000 0.500000 9 | Ke 0.000000 0.000000 0.000000 10 | Ni 1.450000 11 | d 1.000000 12 | illum 2 13 | map_Kd citybits_texture.png 14 | -------------------------------------------------------------------------------- /examples/loadModelMaterials/materials/firehydrant.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/loadModelMaterials/materials/firehydrant.bin -------------------------------------------------------------------------------- /examples/loadModelMaterials/materials/radio.mtl: -------------------------------------------------------------------------------- 1 | # Created by Kenney (www.kenney.nl) 2 | 3 | newmtl metalMedium(Clone) 4 | Kd 0.3686275 0.4666667 0.4666667 5 | 6 | newmtl wood(Clone) 7 | Kd 0.8962265 0.6015712 0.3931559 8 | 9 | newmtl metal(Clone) 10 | Kd 0.7406105 0.8228667 0.8396227 11 | 12 | -------------------------------------------------------------------------------- /examples/loadModelMaterials/materials/watertower.mtl: -------------------------------------------------------------------------------- 1 | # Blender MTL File: 'CityBits_Export.blend' 2 | # Material Count: 1 3 | 4 | newmtl citybits_texture 5 | Ns 359.999993 6 | Ka 1.000000 1.000000 1.000000 7 | Kd 1.000000 1.000000 1.000000 8 | Ks 0.500000 0.500000 0.500000 9 | Ke 0.000000 0.000000 0.000000 10 | Ni 1.450000 11 | d 1.000000 12 | illum 2 13 | map_Kd citybits_texture.png 14 | -------------------------------------------------------------------------------- /examples/loadModelMaterials/simplescene.mtl: -------------------------------------------------------------------------------- 1 | # Created by Kenney (www.kenney.nl) 2 | 3 | newmtl Stone(Clone) 4 | Kd 0.192 0.228 0.246 5 | 6 | newmtl foliage(Clone) 7 | Kd 0.26 0.849 0.603 8 | 9 | newmtl wood(Clone) 10 | Kd 0.896 0.602 0.393 11 | 12 | newmtl BrownDark(Clone) 13 | Kd 0.327 0.102 0.059 14 | 15 | newmtl metal(Clone) 16 | Kd 0.773 0.796 0.906 17 | 18 | newmtl rock(Clone) 19 | Kd 0.915 0.824 0.626 20 | 21 | newmtl woodDark(Clone) 22 | Kd 0.738 0.459 0.254 23 | 24 | newmtl _defaultMat(Clone) 25 | Kd 1 1 1 26 | 27 | newmtl Green(Clone) 28 | Kd 0.065 0.501 0.266 29 | 30 | newmtl GreenDark(Clone) 31 | Kd 0.021 0.257 0.267 32 | 33 | -------------------------------------------------------------------------------- /examples/loadModelMaterials/textures/citybits_texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/loadModelMaterials/textures/citybits_texture.png -------------------------------------------------------------------------------- /examples/material_test/assets/models/castletrack.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/material_test/assets/models/castletrack.glb -------------------------------------------------------------------------------- /examples/material_test/assets/models/castletrack.mtl: -------------------------------------------------------------------------------- 1 | # Created by Kenney (www.kenney.nl) 2 | 3 | newmtl Brown(Clone) 4 | Kd 0.578 0.233 0.115 5 | 6 | newmtl Beige(Clone) 7 | Kd 0.703 0.425 0.207 8 | 9 | newmtl BrownDark(Clone) 10 | Kd 0.327 0.102 0.059 11 | 12 | newmtl Green(Clone) 13 | Kd 0.065 0.501 0.266 14 | 15 | newmtl GreenDark(Clone) 16 | Kd 0.021 0.257 0.267 17 | 18 | newmtl Stone(Clone) 19 | Kd 0.192 0.228 0.246 20 | 21 | -------------------------------------------------------------------------------- /examples/material_test/assets/models/forestTileTower.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/material_test/assets/models/forestTileTower.glb -------------------------------------------------------------------------------- /examples/material_test/assets/models/hex_forest_roadB.gltf.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/material_test/assets/models/hex_forest_roadB.gltf.glb -------------------------------------------------------------------------------- /examples/material_test/assets/models/model.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/material_test/assets/models/model.bin -------------------------------------------------------------------------------- /examples/material_test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/material_test/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | Router, 3 | store, 4 | Level, 5 | Box, 6 | Scene, 7 | Cube, 8 | Controls, 9 | Models, 10 | AmbientLight, 11 | PHYSICS_EVENTS, 12 | constants, 13 | Scripts, 14 | PALETTES, 15 | SunLight, 16 | HemisphereLight, 17 | Sky, 18 | } from "../../dist/mage.js"; 19 | 20 | export default class Intro extends Level { 21 | addAmbientLight() { 22 | const ambientLight = new AmbientLight({ 23 | color: PALETTES.FRENCH_PALETTE.SPRAY, 24 | intensity: 0.5, 25 | }); 26 | 27 | const hemisphereLight = new HemisphereLight({ 28 | color: { 29 | sky: PALETTES.FRENCH_PALETTE.SQUASH_BLOSSOM, 30 | ground: PALETTES.FRENCH_PALETTE.REEF_ENCOUNTER, 31 | }, 32 | intensity: 1, 33 | }); 34 | 35 | const sunLight = new SunLight({ 36 | color: PALETTES.FRENCH_PALETTE.MELON_MELODY, 37 | intensity: 1, 38 | far: 20, 39 | mapSize: 2048, 40 | }); 41 | sunLight.setPosition({ y: 4, z: -3, x: -3 }); 42 | // sunLight.addHelpers(); 43 | } 44 | 45 | createSky() { 46 | const sky = new Sky(); 47 | const inclination = 0.1; 48 | const azimuth = 0.1; 49 | const distance = 100; 50 | 51 | sky.setSun(inclination, azimuth, distance); 52 | } 53 | 54 | onCreate() { 55 | Scene.getCamera().setPosition({ y: 10 }); 56 | Controls.setOrbitControl(); 57 | this.addAmbientLight(); 58 | this.createSky(); 59 | 60 | const tile = Models.getModel("tile"); 61 | 62 | tile.setMaterialFromName(constants.MATERIALS.STANDARD, { roughness: 0.5, metalness: 0 }); 63 | } 64 | } 65 | 66 | const assets = { 67 | "/": { 68 | models: { 69 | tile: "assets/models/hex_forest_roadB.gltf.glb", 70 | }, 71 | }, 72 | }; 73 | 74 | const { SHADOW_TYPES } = constants; 75 | 76 | const config = { 77 | screen: { 78 | h: window ? window.innerHeight : 800, 79 | w: window ? window.innerWidth : 600, 80 | ratio: window ? window.innerWidth / window.innerHeight : 600 / 800, 81 | frameRate: 60, 82 | alpha: true, 83 | }, 84 | 85 | lights: { 86 | shadows: true, 87 | shadowType: SHADOW_TYPES.HARD, 88 | textureAnisotropy: 32, 89 | }, 90 | 91 | physics: { 92 | enabled: true, 93 | path: "dist/ammo.js", 94 | gravity: { x: 0, y: -9.8, z: 0 }, 95 | }, 96 | 97 | tween: { 98 | enabled: false, 99 | }, 100 | 101 | camera: { 102 | fov: 75, 103 | near: 0.1, 104 | far: 3000000, 105 | }, 106 | }; 107 | 108 | window.addEventListener("load", () => { 109 | store.createStore({}, {}, true); 110 | 111 | Router.on("/", Intro); 112 | 113 | Router.start(config, assets); 114 | }); 115 | -------------------------------------------------------------------------------- /examples/particles_custom_emitter/dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/particles_custom_emitter/dot.png -------------------------------------------------------------------------------- /examples/particles_custom_emitter/dot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/particles_custom_emitter/dot_1.png -------------------------------------------------------------------------------- /examples/particles_custom_emitter/fire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/particles_custom_emitter/fire.png -------------------------------------------------------------------------------- /examples/particles_custom_emitter/green_energy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/particles_custom_emitter/green_energy.png -------------------------------------------------------------------------------- /examples/particles_custom_emitter/hex_forest_roadB.gltf.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/particles_custom_emitter/hex_forest_roadB.gltf.glb -------------------------------------------------------------------------------- /examples/particles_custom_emitter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/particles_explosion/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/particles_explosion/.DS_Store -------------------------------------------------------------------------------- /examples/particles_explosion/dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/particles_explosion/dot.png -------------------------------------------------------------------------------- /examples/particles_explosion/dot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/particles_explosion/dot_1.png -------------------------------------------------------------------------------- /examples/particles_explosion/fire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/particles_explosion/fire.png -------------------------------------------------------------------------------- /examples/particles_explosion/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/particles_explosion/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | Router, 3 | store, 4 | Level, 5 | Box, 6 | Scene, 7 | Controls, 8 | AmbientLight, 9 | Particles, 10 | PARTICLES, 11 | Input, 12 | INPUT_EVENTS, 13 | } from "../../dist/mage.js"; 14 | 15 | export default class Intro extends Level { 16 | addAmbientLight() { 17 | this.ambientLight = new AmbientLight({ color: 0xffffff }); 18 | } 19 | 20 | createFloor() { 21 | const floor = new Box(50, 1, 50, 0xffffff); 22 | 23 | floor.setPosition({ y: -5 }); 24 | } 25 | 26 | explode(position = { x: 0, y: 0, z: 0 }) { 27 | console.log("exploding"); 28 | Particles.addParticleEmitter(PARTICLES.EXPLOSION, { 29 | texture: "dot", 30 | hasDebris: true, 31 | }).emit("once"); 32 | } 33 | 34 | onKeyDown({ event }) { 35 | console.log(event); 36 | if (event.key === "e") { 37 | this.explode(); 38 | } 39 | } 40 | 41 | onCreate() { 42 | this.addAmbientLight(); 43 | Controls.setOrbitControl(); 44 | 45 | Input.enable(); 46 | Input.keyboard.addEventListener(INPUT_EVENTS.KEY_DOWN, this.onKeyDown.bind(this)); 47 | 48 | Scene.getCamera().setPosition({ y: 15, z: 45 }); 49 | 50 | this.createFloor(); 51 | } 52 | } 53 | 54 | const assets = { 55 | textures: { 56 | dot: "dot.png", 57 | fire: "fire.png", 58 | }, 59 | }; 60 | 61 | const config = { 62 | screen: { 63 | h: window ? window.innerHeight : 800, 64 | w: window ? window.innerWidth : 600, 65 | ratio: window ? window.innerWidth / window.innerHeight : 600 / 800, 66 | frameRate: 60, 67 | alpha: true, 68 | }, 69 | 70 | lights: { 71 | shadows: true, 72 | }, 73 | 74 | physics: { 75 | enabled: false, 76 | path: "dist/ammo.js", 77 | gravity: { x: 0, y: -9.8, z: 0 }, 78 | }, 79 | 80 | tween: { 81 | enabled: false, 82 | }, 83 | 84 | camera: { 85 | fov: 75, 86 | near: 0.1, 87 | far: 3000000, 88 | }, 89 | }; 90 | 91 | window.addEventListener("load", () => { 92 | store.createStore({}, {}, true); 93 | 94 | Router.on("/", Intro); 95 | 96 | Router.start(config, assets); 97 | }); 98 | -------------------------------------------------------------------------------- /examples/particles_fire/dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/particles_fire/dot.png -------------------------------------------------------------------------------- /examples/particles_fire/dot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/particles_fire/dot_1.png -------------------------------------------------------------------------------- /examples/particles_fire/fire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/particles_fire/fire.png -------------------------------------------------------------------------------- /examples/particles_fire/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/particles_sparkles/dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/particles_sparkles/dot.png -------------------------------------------------------------------------------- /examples/particles_sparkles/dot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/particles_sparkles/dot_1.png -------------------------------------------------------------------------------- /examples/particles_sparkles/fire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/particles_sparkles/fire.png -------------------------------------------------------------------------------- /examples/particles_sparkles/green_energy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/particles_sparkles/green_energy.png -------------------------------------------------------------------------------- /examples/particles_sparkles/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/physics/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/physics/index.js: -------------------------------------------------------------------------------- 1 | import { Router, store, Level, Box, Cube, Sphere, Scene, Controls, Color, PHYSICS_CONSTANTS } from '../../dist/mage.js'; 2 | 3 | class Intro extends Level { 4 | 5 | createCube() { 6 | const size = 2; 7 | const color = Color.randomColor(true); 8 | const cube = new Cube(size, color); 9 | const position = { 10 | x: Math.random() * 30 - 15, 11 | z: Math.random() * 30 - 15, 12 | y: Math.random() * 10 + 10 13 | } 14 | 15 | const rotation = { 16 | x: Math.random(), 17 | y: Math.random(), 18 | z: Math.random() 19 | } 20 | 21 | cube.setPosition(position); 22 | cube.setRotation(rotation); 23 | cube.enablePhysics({ debug: true, mass: 1 }); 24 | } 25 | 26 | createSphere() { 27 | const radius = 2; 28 | const color = Color.randomColor(true); 29 | const sphere = new Sphere(radius, color); 30 | const position = { 31 | x: Math.random() * 30 - 15, 32 | y: Math.random() * 30 - 15, 33 | z: Math.random() * 10 + 10 34 | } 35 | 36 | const rotation = { 37 | x: Math.random(), 38 | y: Math.random(), 39 | z: Math.random() 40 | } 41 | 42 | sphere.setPosition(position); 43 | sphere.setRotation(rotation); 44 | sphere.enablePhysics({ debug: true, mass: 1, colliderType: PHYSICS_CONSTANTS.COLLIDER_TYPES.SPHERE }); 45 | } 46 | 47 | spawnItems(count) { 48 | if (count === 0) return; 49 | 50 | setTimeout(() => { 51 | Math.random() > 0.5 ? this.createCube() : this.createSphere(); 52 | this.spawnItems(count - 1); 53 | }, 500) 54 | } 55 | 56 | onCreate() { 57 | Controls.setOrbitControl(); 58 | 59 | Scene 60 | .getCamera() 61 | .setPosition({ y: 15, z: 45 }); 62 | 63 | const floor = new Box(50, 1, 50, 0xffffff); 64 | 65 | floor.enablePhysics({ mass: 0, debug: true }); 66 | 67 | this.spawnItems(100); 68 | } 69 | } 70 | 71 | const config = { 72 | screen: { 73 | h: window ? window.innerHeight : 800, 74 | w: window ? window.innerWidth : 600, 75 | ratio: window ? window.innerWidth / window.innerHeight : 600 / 800, 76 | frameRate: 60, 77 | alpha: true, 78 | }, 79 | 80 | lights: { 81 | shadows: true, 82 | }, 83 | 84 | physics: { 85 | enabled: true, 86 | path: 'dist/ammo.js' 87 | }, 88 | 89 | tween: { 90 | enabled: false, 91 | }, 92 | 93 | camera: { 94 | fov: 75, 95 | near: 0.1, 96 | far: 3000000, 97 | }, 98 | }; 99 | 100 | window.addEventListener('load', () => { 101 | store.createStore({}, {}, true); 102 | 103 | Router.on('/', Intro); 104 | 105 | Router.start(config, {}); 106 | }); 107 | -------------------------------------------------------------------------------- /examples/rotateTo/assets/models/castletrack.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/rotateTo/assets/models/castletrack.glb -------------------------------------------------------------------------------- /examples/rotateTo/assets/models/castletrack.mtl: -------------------------------------------------------------------------------- 1 | # Created by Kenney (www.kenney.nl) 2 | 3 | newmtl Brown(Clone) 4 | Kd 0.578 0.233 0.115 5 | 6 | newmtl Beige(Clone) 7 | Kd 0.703 0.425 0.207 8 | 9 | newmtl BrownDark(Clone) 10 | Kd 0.327 0.102 0.059 11 | 12 | newmtl Green(Clone) 13 | Kd 0.065 0.501 0.266 14 | 15 | newmtl GreenDark(Clone) 16 | Kd 0.021 0.257 0.267 17 | 18 | newmtl Stone(Clone) 19 | Kd 0.192 0.228 0.246 20 | 21 | -------------------------------------------------------------------------------- /examples/rotateTo/assets/models/forestTileTower.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/rotateTo/assets/models/forestTileTower.glb -------------------------------------------------------------------------------- /examples/rotateTo/assets/models/hex_forest_roadB.gltf.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/rotateTo/assets/models/hex_forest_roadB.gltf.glb -------------------------------------------------------------------------------- /examples/rotateTo/assets/models/model.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/rotateTo/assets/models/model.bin -------------------------------------------------------------------------------- /examples/rotateTo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/scaleTo/assets/models/castletrack.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/scaleTo/assets/models/castletrack.glb -------------------------------------------------------------------------------- /examples/scaleTo/assets/models/castletrack.mtl: -------------------------------------------------------------------------------- 1 | # Created by Kenney (www.kenney.nl) 2 | 3 | newmtl Brown(Clone) 4 | Kd 0.578 0.233 0.115 5 | 6 | newmtl Beige(Clone) 7 | Kd 0.703 0.425 0.207 8 | 9 | newmtl BrownDark(Clone) 10 | Kd 0.327 0.102 0.059 11 | 12 | newmtl Green(Clone) 13 | Kd 0.065 0.501 0.266 14 | 15 | newmtl GreenDark(Clone) 16 | Kd 0.021 0.257 0.267 17 | 18 | newmtl Stone(Clone) 19 | Kd 0.192 0.228 0.246 20 | 21 | -------------------------------------------------------------------------------- /examples/scaleTo/assets/models/forestTileTower.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/scaleTo/assets/models/forestTileTower.glb -------------------------------------------------------------------------------- /examples/scaleTo/assets/models/hex_forest_roadB.gltf.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/scaleTo/assets/models/hex_forest_roadB.gltf.glb -------------------------------------------------------------------------------- /examples/scaleTo/assets/models/model.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/scaleTo/assets/models/model.bin -------------------------------------------------------------------------------- /examples/scaleTo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/scaleTo/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | Router, 3 | store, 4 | Level, 5 | Box, 6 | Scene, 7 | Cube, 8 | Controls, 9 | Models, 10 | AmbientLight, 11 | PHYSICS_EVENTS, 12 | constants, 13 | Scripts, 14 | PALETTES, 15 | SunLight, 16 | HemisphereLight, 17 | Sky, 18 | } from "../../dist/mage.js"; 19 | 20 | export default class Intro extends Level { 21 | addAmbientLight() { 22 | const ambientLight = new AmbientLight({ 23 | color: PALETTES.FRENCH_PALETTE.SPRAY, 24 | intensity: 0.5, 25 | }); 26 | 27 | const hemisphereLight = new HemisphereLight({ 28 | color: { 29 | sky: PALETTES.FRENCH_PALETTE.SQUASH_BLOSSOM, 30 | ground: PALETTES.FRENCH_PALETTE.REEF_ENCOUNTER, 31 | }, 32 | intensity: 1, 33 | }); 34 | 35 | const sunLight = new SunLight({ 36 | color: PALETTES.FRENCH_PALETTE.MELON_MELODY, 37 | intensity: 1, 38 | far: 20, 39 | mapSize: 2048, 40 | }); 41 | sunLight.setPosition({ y: 4, z: -3, x: -3 }); 42 | // sunLight.addHelpers(); 43 | } 44 | 45 | createSky() { 46 | const sky = new Sky(); 47 | const inclination = 0.1; 48 | const azimuth = 0.1; 49 | const distance = 100; 50 | 51 | sky.setSun(inclination, azimuth, distance); 52 | } 53 | 54 | onCreate() { 55 | Scene.getCamera().setPosition({ y: 10 }); 56 | Controls.setOrbitControl(); 57 | this.addAmbientLight(); 58 | this.createSky(); 59 | 60 | const cube = new Cube(5, 0xffeeaa); 61 | 62 | cube.setMaterialFromName(constants.MATERIALS.STANDARD, { roughness: 0.5, metalness: 0 }); 63 | setTimeout(() => { 64 | cube.scaleTo({ x: 4, y: 4, z: 4 }, 5000, { loop: "bounce" }); 65 | }, 2000); 66 | } 67 | } 68 | 69 | const assets = {}; 70 | 71 | const { SHADOW_TYPES } = constants; 72 | 73 | const config = { 74 | screen: { 75 | h: window ? window.innerHeight : 800, 76 | w: window ? window.innerWidth : 600, 77 | ratio: window ? window.innerWidth / window.innerHeight : 600 / 800, 78 | frameRate: 60, 79 | alpha: true, 80 | }, 81 | 82 | lights: { 83 | shadows: true, 84 | shadowType: SHADOW_TYPES.HARD, 85 | textureAnisotropy: 32, 86 | }, 87 | 88 | physics: { 89 | enabled: true, 90 | path: "dist/ammo.js", 91 | gravity: { x: 0, y: -9.8, z: 0 }, 92 | }, 93 | 94 | tween: { 95 | enabled: false, 96 | }, 97 | 98 | camera: { 99 | fov: 75, 100 | near: 0.1, 101 | far: 3000000, 102 | }, 103 | }; 104 | 105 | window.addEventListener("load", () => { 106 | store.createStore({}, {}, true); 107 | 108 | Router.on("/", Intro); 109 | 110 | Router.start(config, assets); 111 | }); 112 | -------------------------------------------------------------------------------- /examples/selection_effect/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/sound/click.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/sound/click.ogg -------------------------------------------------------------------------------- /examples/sound/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/sound/index.js: -------------------------------------------------------------------------------- 1 | import { Router, store, Level, Box, Scene, Controls, AmbientLight, AmbientSound, Input, INPUT_EVENTS } from '../../dist/mage.js'; 2 | 3 | export default class Example extends Level { 4 | 5 | addAmbientLight() { 6 | this.ambientLight = new AmbientLight({ color: 0xffffff }); 7 | } 8 | 9 | createFloor() { 10 | const floor = new Box(50, 1, 50, 0xffffff); 11 | 12 | floor.setPosition({ y: -5 }); 13 | } 14 | 15 | playSound() { 16 | new AmbientSound('click', { autoplay: true }); 17 | } 18 | 19 | onKeyDown({ event }) { 20 | console.log(event); 21 | if (event.key === 'e') { 22 | this.playSound(); 23 | } 24 | } 25 | 26 | onCreate() { 27 | this.addAmbientLight(); 28 | Controls.setOrbitControl(); 29 | 30 | Scene 31 | .getCamera() 32 | .setPosition({ y: 15, z: 45 }); 33 | Input.enable(); 34 | Input.keyboard.addEventListener(INPUT_EVENTS.KEY_DOWN, this.onKeyDown.bind(this)); 35 | 36 | // this.createFloor();/ 37 | } 38 | } 39 | 40 | const assets = { 41 | audio: { 42 | 'click': 'click.ogg' 43 | } 44 | }; 45 | 46 | const config = { 47 | screen: { 48 | h: window ? window.innerHeight : 800, 49 | w: window ? window.innerWidth : 600, 50 | ratio: window ? window.innerWidth / window.innerHeight : 600 / 800, 51 | frameRate: 60, 52 | alpha: true, 53 | }, 54 | 55 | lights: { 56 | shadows: true, 57 | }, 58 | 59 | physics: { 60 | enabled: false, 61 | path: 'dist/ammo.js', 62 | gravity: { x: 0, y: -9.8, z: 0} 63 | }, 64 | 65 | tween: { 66 | enabled: false, 67 | }, 68 | 69 | camera: { 70 | fov: 75, 71 | near: 0.1, 72 | far: 3000000, 73 | }, 74 | }; 75 | 76 | window.addEventListener('load', () => { 77 | store.createStore({}, {}, true); 78 | 79 | Router.on('/', Example); 80 | 81 | Router.start(config, assets); 82 | }); 83 | -------------------------------------------------------------------------------- /examples/spotlight/assets/models/Textures/roof(Clone).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/spotlight/assets/models/Textures/roof(Clone).png -------------------------------------------------------------------------------- /examples/spotlight/assets/models/lamp.mtl: -------------------------------------------------------------------------------- 1 | # Created by Kenney (www.kenney.nl) 2 | 3 | newmtl roof(Clone) 4 | Kd 1 1 1 5 | map_Kd Textures/roof(Clone).png 6 | 7 | -------------------------------------------------------------------------------- /examples/spotlight/assets/textures/ambientlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/spotlight/assets/textures/ambientlight.png -------------------------------------------------------------------------------- /examples/spotlight/assets/textures/hemispherelight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/spotlight/assets/textures/hemispherelight.png -------------------------------------------------------------------------------- /examples/spotlight/assets/textures/lightbulb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/spotlight/assets/textures/lightbulb.png -------------------------------------------------------------------------------- /examples/spotlight/assets/textures/pointlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/spotlight/assets/textures/pointlight.png -------------------------------------------------------------------------------- /examples/spotlight/assets/textures/spotlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/spotlight/assets/textures/spotlight.png -------------------------------------------------------------------------------- /examples/spotlight/assets/textures/sunlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/spotlight/assets/textures/sunlight.png -------------------------------------------------------------------------------- /examples/spotlight/assets/textures/target.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/spotlight/assets/textures/target.png -------------------------------------------------------------------------------- /examples/spotlight/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/sunlight/assets/models/Textures/roof(Clone).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/sunlight/assets/models/Textures/roof(Clone).png -------------------------------------------------------------------------------- /examples/sunlight/assets/models/lamp.mtl: -------------------------------------------------------------------------------- 1 | # Created by Kenney (www.kenney.nl) 2 | 3 | newmtl roof(Clone) 4 | Kd 1 1 1 5 | map_Kd Textures/roof(Clone).png 6 | 7 | -------------------------------------------------------------------------------- /examples/sunlight/assets/textures/ambientlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/sunlight/assets/textures/ambientlight.png -------------------------------------------------------------------------------- /examples/sunlight/assets/textures/hemispherelight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/sunlight/assets/textures/hemispherelight.png -------------------------------------------------------------------------------- /examples/sunlight/assets/textures/lightbulb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/sunlight/assets/textures/lightbulb.png -------------------------------------------------------------------------------- /examples/sunlight/assets/textures/pointlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/sunlight/assets/textures/pointlight.png -------------------------------------------------------------------------------- /examples/sunlight/assets/textures/spotlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/sunlight/assets/textures/spotlight.png -------------------------------------------------------------------------------- /examples/sunlight/assets/textures/sunlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/sunlight/assets/textures/sunlight.png -------------------------------------------------------------------------------- /examples/sunlight/assets/textures/target.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/sunlight/assets/textures/target.png -------------------------------------------------------------------------------- /examples/sunlight/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/textures/assets/Wood_025_ambientOcclusion.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/textures/assets/Wood_025_ambientOcclusion.jpg -------------------------------------------------------------------------------- /examples/textures/assets/Wood_025_basecolor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/textures/assets/Wood_025_basecolor.jpg -------------------------------------------------------------------------------- /examples/textures/assets/Wood_025_height.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/textures/assets/Wood_025_height.png -------------------------------------------------------------------------------- /examples/textures/assets/Wood_025_normal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/textures/assets/Wood_025_normal.jpg -------------------------------------------------------------------------------- /examples/textures/assets/Wood_025_roughness.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/textures/assets/Wood_025_roughness.jpg -------------------------------------------------------------------------------- /examples/textures/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/trails/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/trails/index.js: -------------------------------------------------------------------------------- 1 | import { Router, store, Level, Cube, Scene, Controls, AmbientLight, Particles, PARTICLES, BaseScript, Scripts, BUILTIN } from '../../dist/mage.js'; 2 | 3 | class SimpleScript extends BaseScript { 4 | constructor() { super('SimpleScript'); } 5 | 6 | start(element) { 7 | this.element = element; 8 | this.angle = 0; 9 | } 10 | 11 | update(dt) { 12 | this.angle += 5 * dt; 13 | 14 | this.element.setPosition({ x: Math.sin(this.angle) * 10 }); 15 | } 16 | } 17 | 18 | export default class Intro extends Level { 19 | 20 | addAmbientLight() { 21 | this.ambientLight = new AmbientLight({ color: 0xffffff }); 22 | } 23 | 24 | createCube() { 25 | const cube = new Cube(1, 0xeeeaaa); 26 | 27 | cube.addScript('SimpleScript'); 28 | const trail = Particles.addParticleEmitter(PARTICLES.TRAIL, { texture: 'particle' }); 29 | trail.emit(Infinity); 30 | cube.add(trail); 31 | 32 | // trail.addScript('SimpleScript'); 33 | 34 | 35 | cube.setWireframe(true); 36 | } 37 | 38 | onCreate() { 39 | Scripts.create('SimpleScript', SimpleScript); 40 | this.addAmbientLight(); 41 | Controls.setOrbitControl(); 42 | 43 | this.createCube(); 44 | 45 | Scene 46 | .getCamera() 47 | .setPosition({ y: 15, z: 45 }); 48 | } 49 | } 50 | 51 | const assets = { 52 | textures: { 53 | particle: 'particle.png' 54 | } 55 | }; 56 | 57 | const config = { 58 | screen: { 59 | h: window ? window.innerHeight : 800, 60 | w: window ? window.innerWidth : 600, 61 | ratio: window ? window.innerWidth / window.innerHeight : 600 / 800, 62 | frameRate: 60, 63 | alpha: true, 64 | }, 65 | 66 | lights: { 67 | shadows: true, 68 | }, 69 | 70 | physics: { 71 | enabled: false, 72 | path: 'dist/ammo.js', 73 | gravity: { x: 0, y: -9.8, z: 0} 74 | }, 75 | 76 | tween: { 77 | enabled: false, 78 | }, 79 | 80 | camera: { 81 | fov: 75, 82 | near: 0.1, 83 | far: 3000000, 84 | }, 85 | }; 86 | 87 | window.addEventListener('load', () => { 88 | store.createStore({}, {}, true); 89 | 90 | Router.on('/', Intro); 91 | 92 | Router.start(config, assets); 93 | }); 94 | -------------------------------------------------------------------------------- /examples/trails/particle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/examples/trails/particle.png -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/src/.DS_Store -------------------------------------------------------------------------------- /src/audio/ambientSound.js: -------------------------------------------------------------------------------- 1 | import { ENTITY_TYPES } from "../entities/constants"; 2 | import { generateRandomName } from "../lib/uuid"; 3 | import Sound from "./sound"; 4 | export default class AmbientSound extends Sound { 5 | constructor(source, { name = generateRandomName("AmbientSound"), ...options } = {}) { 6 | super({ source, name, ...options }); 7 | this.setEntityType(ENTITY_TYPES.AUDIO.AMBIENT); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/controls/constants.js: -------------------------------------------------------------------------------- 1 | export const CONTROLS = { 2 | ORBIT: "CONTROLS:ORBIT", 3 | TRANSFORM: "CONTROLS:TRANSFORM", 4 | FPS: "CONTROLS:FPS", 5 | FLY: "CONTROLS:FLY", 6 | }; 7 | 8 | export const AVAILABLE_CONTROLS = Object.keys(CONTROLS); 9 | -------------------------------------------------------------------------------- /src/core/Stats.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author mrdoob / http://mrdoob.com/ 3 | */ 4 | import { Subject } from 'rxjs'; 5 | import features, { FEATURES } from '../lib/features'; 6 | 7 | const ONE_MB = 1048576; 8 | 9 | class Stats { 10 | 11 | constructor() { 12 | this.beginTime = (performance || Date).now(); 13 | this.prevTime = this.beginTime; 14 | 15 | this.frames = 0; 16 | this._fps = 0; 17 | this._fpsMax = 100; 18 | this.fps = new Subject(); 19 | this.memory = new Subject(); 20 | this.memoryMax = new Subject(); 21 | } 22 | 23 | init() { 24 | this.beginTime = (performance || Date).now(); 25 | } 26 | 27 | getFPS() { 28 | this.frames++; 29 | const time = (performance || Date).now(); 30 | 31 | if (time >= this.prevTime + 1000) { 32 | this._fps = (this.frames * 1000)/(time - this.prevTime); 33 | this.prevTime = time; 34 | this.frames = 0; 35 | this.fps.next(this._fps); 36 | } 37 | 38 | this.beginTime = time; 39 | 40 | return this._fps; 41 | } 42 | 43 | subscribe(handler) { 44 | this.fps.subscribe(handler); 45 | 46 | return () => this.fps.unsubscribe(handler); 47 | } 48 | 49 | getMemoryUsage() { 50 | if (features.isFeatureSupported(FEATURES.MEMORY)) { 51 | this.memory.next(performance.memory.usedJSHeapSize / ONE_MB); 52 | this.memoryMax.next(performance.memory.jsHeapSizeLimit / ONE_MB); 53 | } 54 | } 55 | 56 | update() { 57 | this.getFPS(); 58 | } 59 | } 60 | 61 | export default new Stats(); 62 | -------------------------------------------------------------------------------- /src/core/input/Keyboard.js: -------------------------------------------------------------------------------- 1 | import hotkeys from "hotkeys-js"; 2 | import { KEYBOARD_COMBO_ALREADY_REGISTERED, KEYBOARD_COMBO_IS_INVALID } from "../../lib/messages"; 3 | 4 | import { EventDispatcher } from "three"; 5 | 6 | const HOTKEYS_DEFAULT_OPTIONS = { 7 | keyup: true, 8 | keydown: true, 9 | }; 10 | 11 | const COMBINATION_DIVIDER = ","; 12 | 13 | export const KEY_PRESS = "keyPress"; 14 | export const KEY_DOWN = "keyDown"; 15 | export const KEY_UP = "keyUp"; 16 | export const KEY_COMBO = "keyCombo"; 17 | export default class Keyboard extends EventDispatcher { 18 | constructor() { 19 | super(); 20 | 21 | this.combos = []; 22 | 23 | this.enabled = false; 24 | } 25 | 26 | listenTo(combos = []) { 27 | if (this.enabled) { 28 | if (!(combos instanceof Array) || !combos.length) { 29 | console.warn(KEYBOARD_COMBO_IS_INVALID); 30 | } 31 | const parsedCombos = combos.join(COMBINATION_DIVIDER); 32 | 33 | hotkeys(parsedCombos, HOTKEYS_DEFAULT_OPTIONS, this.handleCombo); 34 | } 35 | } 36 | 37 | handleCombo = event => { 38 | this.dispatchEvent({ 39 | type: KEY_COMBO, 40 | event, 41 | }); 42 | }; 43 | 44 | handleKeydown = event => { 45 | this.dispatchEvent({ 46 | type: KEY_DOWN, 47 | event, 48 | }); 49 | }; 50 | 51 | handleKeyup = event => { 52 | this.dispatchEvent({ 53 | type: KEY_UP, 54 | event, 55 | }); 56 | }; 57 | 58 | handleKeypress = event => { 59 | this.dispatchEvent({ 60 | type: KEY_PRESS, 61 | event, 62 | }); 63 | }; 64 | 65 | enable(options = {}) { 66 | this.enabled = true; 67 | 68 | if (options.combos) { 69 | this.listenTo(options.combos); 70 | } 71 | 72 | window.addEventListener(KEY_DOWN.toLowerCase(), this.handleKeydown.bind(this)); 73 | window.addEventListener(KEY_UP.toLowerCase(), this.handleKeyup.bind(this)); 74 | window.addEventListener(KEY_PRESS.toLowerCase(), this.handleKeypress.bind(this)); 75 | } 76 | 77 | disable() { 78 | this.enabled = false; 79 | 80 | hotkeys.unbind(); 81 | window.removeEventListener(KEY_DOWN.toLowerCase(), this.handleKeydown.bind(this)); 82 | window.removeEventListener(KEY_UP.toLowerCase(), this.handleKeyup.bind(this)); 83 | window.removeEventListener(KEY_PRESS.toLowerCase(), this.handleKeypress.bind(this)); 84 | } 85 | 86 | isPressed(key) { 87 | console.log(key, hotkeys.getPressedKeyCodes()); 88 | return hotkeys.isPressed(key); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/core/input/constants.js: -------------------------------------------------------------------------------- 1 | export const STANDARD = 'STANDARD'; 2 | 3 | export const GAMEPAD_BUTTON_MAPPINGS = { 4 | [STANDARD]: [ 5 | 'X', 6 | 'B', 7 | 'A', 8 | 'Y', 9 | 'L1', 10 | 'R1', 11 | null, 12 | null, 13 | 'SELECT', 14 | 'START' 15 | ] 16 | } -------------------------------------------------------------------------------- /src/core/universe.js: -------------------------------------------------------------------------------- 1 | export class Universe { 2 | constructor() { 3 | this.reality = {}; 4 | this.realityUUID = {}; 5 | this.worker = undefined; 6 | } 7 | 8 | get(id) { 9 | return this.reality[id]; 10 | } 11 | 12 | find(element) { 13 | if (!element) return; 14 | 15 | let found; 16 | this.forEach(el => { 17 | if (el.has(element) && !found) { 18 | found = el; 19 | } 20 | }); 21 | 22 | return found; 23 | } 24 | 25 | getByUUID(uuid) { 26 | const id = this.realityUUID[uuid.toString()]; 27 | 28 | if (id) { 29 | return this.get(id); 30 | } 31 | } 32 | 33 | getByTag(tagName) { 34 | const elements = []; 35 | this.forEach(element => { 36 | if (element.hasTag(tagName)) { 37 | elements.push(element); 38 | } 39 | }); 40 | 41 | return elements; 42 | } 43 | 44 | set(id, value) { 45 | this.reality[id] = value; 46 | } 47 | 48 | reset = () => { 49 | this.reality = {}; 50 | this.realityUUID = {}; 51 | }; 52 | 53 | storeUUIDToElementNameReference(uuid, name) { 54 | this.realityUUID[uuid] = name; 55 | } 56 | 57 | remove(id) { 58 | delete this.reality[id]; 59 | } 60 | 61 | forEach = callback => { 62 | Object.keys(this.reality).forEach(k => callback(this.reality[k], k)); 63 | }; 64 | 65 | forEachAsync = callback => { 66 | const keys = Object.keys(this.reality); 67 | 68 | return new Promise(resolve => { 69 | Promise.all(keys.map(k => callback(this.reality[k], k))).then(resolve); 70 | }); 71 | }; 72 | 73 | update(dt) { 74 | Object.keys(this.reality).forEach(k => this.reality[k].update(dt)); 75 | } 76 | 77 | onPhysicsUpdate(dt) { 78 | Object.keys(this.reality).forEach(k => this.reality[k].onPhysicsUpdate(dt)); 79 | } 80 | 81 | bigfreeze = () => { 82 | this.forEach(o => { 83 | o && o.dispose && o.dispose(); 84 | }); 85 | this.reset(); 86 | }; 87 | 88 | toJSON(parseJSON = false) { 89 | const elements = Object.keys(this.reality) 90 | .map(k => this.get(k)) 91 | .filter(element => element.serializable && element.isMesh()) 92 | .map(element => element.toJSON(parseJSON)); 93 | 94 | return { elements }; 95 | } 96 | } 97 | 98 | export default new Universe(); 99 | -------------------------------------------------------------------------------- /src/core/window.js: -------------------------------------------------------------------------------- 1 | export const getWindow = () => { 2 | try { 3 | const win = window || global.window; 4 | 5 | return win; 6 | } catch (e) { 7 | return null; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/entities/animations/AnimationHandler.js: -------------------------------------------------------------------------------- 1 | import { AnimationMixer, AnimationClip, LoopRepeat, EventDispatcher } from 'three'; 2 | import { ENTITY_EVENTS } from '../constants'; 3 | import { ANIMATION_NOT_FOUND } from '../../lib/messages'; 4 | 5 | export default class AnimationHandler extends EventDispatcher { 6 | 7 | constructor(mesh, animations = []) { 8 | super(); 9 | this.mixer = new AnimationMixer(mesh); 10 | this.animations = animations; 11 | this.isPlaying = false; 12 | this.addEventsListeners(); 13 | } 14 | 15 | addEventsListeners() { 16 | this.mixer.addEventListener('loop', this.getAnimationEventHandler(ENTITY_EVENTS.ANIMATION.LOOP)); 17 | this.mixer.addEventListener('finished', this.getAnimationEventHandler(ENTITY_EVENTS.ANIMATION.FINISHED)); 18 | } 19 | 20 | getAnimationEventHandler = (type) => ({ action, direction }) => { 21 | this.dispatchEvent({ 22 | type, 23 | action, 24 | direction 25 | }) 26 | } 27 | 28 | getAction(id) { 29 | let action; 30 | 31 | if (typeof id === 'number') { 32 | action = this.animations[id]; 33 | } else if (typeof id === 'string') { 34 | action = AnimationClip.findByName(this.animations, id); 35 | } 36 | 37 | return action; 38 | } 39 | 40 | getAvailableAnimations() { 41 | return this.animations.map(({ name }) => name); 42 | } 43 | 44 | stopAll() { 45 | this.mixer.stopAllAction(); 46 | this.isPlaying = false; 47 | } 48 | 49 | stopCurrentAnimation() { 50 | if (this.currentAction) { 51 | this.currentAction.stop(); 52 | } 53 | 54 | this.isPlaying = false; 55 | } 56 | 57 | playAnimation(id, options) { 58 | const action = this.getAction(id); 59 | const { loop = LoopRepeat } = options; 60 | this.isPlaying = true; 61 | 62 | if (this.currentAction) { 63 | this.fadeToAnimation(action, options); 64 | } else if (action) { 65 | this.currentAction = this.mixer 66 | .clipAction(action) 67 | .setLoop(loop) 68 | .play(); 69 | } else { 70 | console.warn(ANIMATION_NOT_FOUND); 71 | } 72 | } 73 | 74 | fadeToAnimation(action, { duration = 0.2, loop = LoopRepeat } = {}) { 75 | const previousAction = this.currentAction; 76 | this.currentAction = this.mixer.clipAction(action); 77 | 78 | if (previousAction !== this.currentAction) { 79 | previousAction.fadeOut(duration); 80 | } 81 | 82 | this.currentAction 83 | .reset() 84 | .setEffectiveTimeScale(1) 85 | .setEffectiveWeight(1) 86 | .fadeIn(duration) 87 | .setLoop(loop) 88 | .play(); 89 | } 90 | 91 | update(dt) { 92 | this.mixer.update(dt); 93 | } 94 | } -------------------------------------------------------------------------------- /src/entities/animations/SpriteAnimationHandler.js: -------------------------------------------------------------------------------- 1 | export default class SpriteAnimationHandler { 2 | 3 | }; -------------------------------------------------------------------------------- /src/entities/animations/lib/NodeFrame.js: -------------------------------------------------------------------------------- 1 | export default class NodeFrame { 2 | 3 | constructor(time) { 4 | this.time = time !== undefined ? time : 0; 5 | this.id = 0; 6 | } 7 | 8 | update( delta ) { 9 | ++ this.id; 10 | 11 | his.time += delta; 12 | this.delta = delta; 13 | 14 | return this; 15 | } 16 | 17 | setRenderer( renderer ) { 18 | 19 | this.renderer = renderer; 20 | 21 | return this; 22 | 23 | } 24 | 25 | setRenderTexture( renderTexture ) { 26 | 27 | this.renderTexture = renderTexture; 28 | return this; 29 | 30 | } 31 | 32 | updateNode( node ) { 33 | 34 | if ( node.frameId === this.id ) return this; 35 | 36 | node.updateFrame( this ); 37 | 38 | node.frameId = this.id; 39 | 40 | return this; 41 | 42 | } 43 | 44 | }; -------------------------------------------------------------------------------- /src/entities/base/AnimatedSprite.js: -------------------------------------------------------------------------------- 1 | import Element from '../Element'; 2 | import { ENTITY_TYPES } from '../constants'; 3 | import { Images } from "../../images/Images"; 4 | 5 | import { 6 | THREESprite, 7 | SpriteNodeMaterial 8 | } from 'three'; 9 | 10 | export default class AnimatedSprite extends Element { 11 | 12 | constructor(width = 20, height = 20, spriteTexture, options = {}) { 13 | super(options); 14 | 15 | const material = new SpriteNodeMaterial({ 16 | map: Images.get(spriteTexture), 17 | ...options 18 | }); 19 | 20 | const body = new THREESprite(material); 21 | body.scale.x = width; 22 | body.scale.y = height; 23 | 24 | this.setBody({ body }); 25 | this.setEntityType(ENTITY_TYPES.SPRITE); 26 | } 27 | } -------------------------------------------------------------------------------- /src/entities/base/Axes.js: -------------------------------------------------------------------------------- 1 | import Element from '../Element'; 2 | import { ENTITY_TYPES } from '../constants'; 3 | import { 4 | LineSegments, 5 | LineBasicMaterial, 6 | Float32BufferAttribute, 7 | Color, 8 | BufferGeometry 9 | } from 'three'; 10 | import { generateRandomName } from '../../lib/uuid'; 11 | 12 | class AxesHelper extends LineSegments { 13 | 14 | constructor( size = 1, thickness = 1 ) { 15 | 16 | const vertices = [ 17 | 0, 0, 0, size, 0, 0, 18 | 0, 0, 0, 0, size, 0, 19 | 0, 0, 0, 0, 0, size 20 | ]; 21 | 22 | const colors = [ 23 | 1, 0, 0, 1, 0.6, 0, 24 | 0, 1, 0, 0.6, 1, 0, 25 | 0, 0, 1, 0, 0.6, 1 26 | ]; 27 | 28 | const geometry = new BufferGeometry(); 29 | geometry.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) ); 30 | geometry.setAttribute( 'color', new Float32BufferAttribute( colors, 3 ) ); 31 | 32 | const material = new LineBasicMaterial( { vertexColors: true, toneMapped: false, linewidth: thickness } ); 33 | 34 | super( geometry, material ); 35 | 36 | this.type = 'AxesHelper'; 37 | } 38 | 39 | setColors( xAxisColor, yAxisColor, zAxisColor ) { 40 | const color = new Color(); 41 | const array = this.geometry.attributes.color.array; 42 | 43 | color.set( xAxisColor ); 44 | color.toArray( array, 0 ); 45 | color.toArray( array, 3 ); 46 | 47 | color.set( yAxisColor ); 48 | color.toArray( array, 6 ); 49 | color.toArray( array, 9 ); 50 | 51 | color.set( zAxisColor ); 52 | color.toArray( array, 12 ); 53 | color.toArray( array, 15 ); 54 | 55 | this.geometry.attributes.color.needsUpdate = true; 56 | 57 | return this; 58 | } 59 | 60 | dispose() { 61 | this.geometry.dispose(); 62 | this.material.dispose(); 63 | } 64 | 65 | } 66 | 67 | 68 | export default class Axes extends Element { 69 | 70 | constructor(size, thickness) { 71 | const options = { 72 | size, 73 | thickness, 74 | name: generateRandomName('AxesHelper') 75 | }; 76 | 77 | super(options); 78 | const body = new AxesHelper(size, thickness); 79 | 80 | this.setBody({ body }); 81 | this.setEntityType(ENTITY_TYPES.HELPER.AXES); 82 | } 83 | 84 | update() {} 85 | } 86 | -------------------------------------------------------------------------------- /src/entities/base/Box.js: -------------------------------------------------------------------------------- 1 | import { BoxGeometry, MeshBasicMaterial } from "three"; 2 | import Element from "../Element"; 3 | import { ENTITY_TYPES } from "../constants"; 4 | import Color from "../../lib/Color"; 5 | 6 | export default class Box extends Element { 7 | constructor( 8 | width = 10, 9 | height = 10, 10 | depth = 10, 11 | color = Color.randomColor(true), 12 | options = {}, 13 | ) { 14 | super(options); 15 | 16 | const geometry = new BoxGeometry(width, height, depth); 17 | const material = new MeshBasicMaterial({ 18 | color, 19 | wireframe: false, 20 | ...options, 21 | }); 22 | 23 | this.setBody({ geometry, material }); 24 | this.setEntityType(ENTITY_TYPES.MESH); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/entities/base/Cone.js: -------------------------------------------------------------------------------- 1 | import { 2 | ConeGeometry, 3 | MeshBasicMaterial 4 | } from 'three'; 5 | import Color from '../../lib/Color'; 6 | import Element from '../Element'; 7 | import { ENTITY_TYPES } from '../constants'; 8 | 9 | const DEFAULT_RADIUS = 5; 10 | const DEFAULT_HEIGHT = 5; 11 | const DEFAULT_RADIAL_SEGMENTS = 8; 12 | const DEFAULT_HEIGHT_SEGMENTS = 1; 13 | const DEFAULT_OPENENDED = false; 14 | 15 | export default class Cone extends Element { 16 | 17 | constructor(radius = DEFAULT_RADIUS, height = DEFAULT_HEIGHT, color = Color.randomColor(true), options = {}) { 18 | super(options); 19 | 20 | const { 21 | radialSegments = DEFAULT_RADIAL_SEGMENTS, 22 | heightSegments = DEFAULT_HEIGHT_SEGMENTS, 23 | openEnded = DEFAULT_OPENENDED 24 | } = options; 25 | 26 | const geometry = new ConeGeometry(radius, height, radialSegments, heightSegments, openEnded); 27 | const material = new MeshBasicMaterial({ 28 | color, 29 | wireframe: false, 30 | ...options 31 | }); 32 | 33 | this.setBody({ geometry, material }); 34 | this.setEntityType(ENTITY_TYPES.MESH); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/entities/base/Cube.js: -------------------------------------------------------------------------------- 1 | import { BoxGeometry, MeshBasicMaterial } from "three"; 2 | import Element from "../Element"; 3 | import { ENTITY_TYPES } from "../constants"; 4 | import Color from "../../lib/Color"; 5 | 6 | export default class Cube extends Element { 7 | constructor(side = 10, color = Color.randomColor(true), options = {}) { 8 | super(options); 9 | 10 | const geometry = new BoxGeometry(side, side, side); 11 | const material = new MeshBasicMaterial({ 12 | color, 13 | wireframe: false, 14 | ...options, 15 | }); 16 | 17 | this.setBody({ geometry, material }); 18 | this.setEntityType(ENTITY_TYPES.MESH); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/entities/base/CurveLine.js: -------------------------------------------------------------------------------- 1 | 2 | import { Vector3, CatmullRomCurve3 } from 'three'; 3 | import Line from './Line'; 4 | 5 | const DEFAULT_CURVE_LINE_DIVISIONS = 20; 6 | 7 | export default class CurveLine extends Line { 8 | 9 | constructor(points = [], options = {}) { 10 | const { 11 | divisions = DEFAULT_CURVE_LINE_DIVISIONS 12 | } = options; 13 | const curve = CurveLine.genereateCurve(points, divisions); 14 | 15 | super(curve, options); 16 | 17 | this.curve = curve; 18 | } 19 | 20 | static genereateCurve(points, divisions) { 21 | return new CatmullRomCurve3( 22 | points.map(({ x, y, z }) => new Vector3(x, y, z)) 23 | ).getPoints(divisions); 24 | } 25 | }; -------------------------------------------------------------------------------- /src/entities/base/Cylinder.js: -------------------------------------------------------------------------------- 1 | import { CylinderGeometry, MeshBasicMaterial } from "three"; 2 | import Element from "../Element"; 3 | import { ENTITY_TYPES } from "../constants"; 4 | import Color from "../../lib/Color"; 5 | 6 | export default class Cylinder extends Element { 7 | constructor( 8 | radiusTop = 10, 9 | radiusBottom = 10, 10 | height = 10, 11 | color = Color.randomColor(true), 12 | options = {}, 13 | ) { 14 | super(options); 15 | 16 | const segments = 32; 17 | 18 | const geometry = new CylinderGeometry(radiusTop, radiusBottom, height, segments); 19 | const material = new MeshBasicMaterial({ 20 | color, 21 | wireframe: false, 22 | ...options, 23 | }); 24 | 25 | this.setBody({ geometry, material }); 26 | this.setEntityType(ENTITY_TYPES.MESH); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/entities/base/Grid.js: -------------------------------------------------------------------------------- 1 | import { GridHelper } from "three"; 2 | import Element from "../Element"; 3 | import { ENTITY_TYPES } from "../constants"; 4 | import { generateRandomName } from "../../lib/uuid"; 5 | import Color from "../../lib/Color"; 6 | 7 | export default class Grid extends Element { 8 | constructor( 9 | size, 10 | division, 11 | color1 = Color.randomColor(true), 12 | color2 = Color.randomColor(true), 13 | ) { 14 | const options = { 15 | size, 16 | division, 17 | color1, 18 | color2, 19 | name: generateRandomName("GridHelper"), 20 | }; 21 | 22 | super(options); 23 | const body = new GridHelper(size, division, color1, color2); 24 | 25 | this.setBody({ body }); 26 | this.setEntityType(ENTITY_TYPES.HELPER.GRID); 27 | } 28 | 29 | update() {} 30 | } 31 | -------------------------------------------------------------------------------- /src/entities/base/HelperSprite.js: -------------------------------------------------------------------------------- 1 | import Sprite from "./Sprite"; 2 | 3 | export default class HelperSprite extends Sprite { 4 | constructor(...options) { 5 | super(...options); 6 | 7 | this.helperTarget = undefined; 8 | } 9 | 10 | setHelperTarget(element) { 11 | this.helperTarget = element; 12 | } 13 | 14 | getHelperTarget() { 15 | return this.helperTarget; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/entities/base/Line.js: -------------------------------------------------------------------------------- 1 | import Element from '../Element'; 2 | import { ENTITY_TYPES } from '../constants'; 3 | 4 | import { 5 | LineBasicMaterial, 6 | LineDashedMaterial, 7 | BufferGeometry, 8 | Line as THREELine, 9 | Vector3 10 | } from 'three'; 11 | 12 | const DEFAULT_LINE_COLOR = 0xffffff; 13 | const DEFAULT_LINE_THICKNESS = 2; 14 | 15 | export default class Line extends Element { 16 | 17 | constructor(points = [], options = {}) { 18 | super(options); 19 | 20 | this.points = points; 21 | 22 | const material = this.getMaterial(); 23 | const geometry = this.getGeometryFromPoints(); 24 | const body = new THREELine(geometry, material); 25 | 26 | this.setBody({ body }); 27 | this.setEntityType(ENTITY_TYPES.MESH); 28 | } 29 | 30 | postBodyCreation() { 31 | super.postBodyCreation(); 32 | const { dashed = false } = this.options; 33 | 34 | if (dashed) { 35 | this.body.computeLineDistances(); 36 | } 37 | } 38 | 39 | getMaterial() { 40 | const { 41 | color = DEFAULT_LINE_COLOR, 42 | dashed = false, 43 | thickness = DEFAULT_LINE_THICKNESS 44 | } = this.options; 45 | 46 | return dashed ? 47 | new LineDashedMaterial({ color, linewidth: thickness }) : 48 | new LineBasicMaterial({ color, linewidth: thickness }); 49 | } 50 | 51 | getGeometryFromPoints() { 52 | const vectors = this.points.map(({ x = 0, y = 0, z = 0}) => new Vector3(x, y, z)); 53 | 54 | return new BufferGeometry() 55 | .setFromPoints(vectors); 56 | } 57 | 58 | updatePoints = (points) => { 59 | const vectors = points.map(({ x, y, z }) => new Vector3(x, y, z)); 60 | this.body.geometry.vertices = vectors; 61 | this.body.geometry.verticesNeedUpdate = true; 62 | } 63 | 64 | setThickness(thickness = DEFAULT_LINE_THICKNESS) { 65 | this.body.material.linewidth = thickness; 66 | } 67 | } -------------------------------------------------------------------------------- /src/entities/base/Plane.js: -------------------------------------------------------------------------------- 1 | import Element from "../Element"; 2 | import { ENTITY_TYPES } from "../constants"; 3 | 4 | import { PlaneGeometry, DoubleSide, Vector3, MeshBasicMaterial } from "three"; 5 | import Color from "../../lib/Color"; 6 | 7 | const UP = new Vector3(0, 1, 0); 8 | const DOWN = new Vector3(0, -1, 0); 9 | 10 | export default class Plane extends Element { 11 | constructor(height, width, color = Color.randomColor(true), options = {}) { 12 | super(options); 13 | 14 | const { transparent = false, opacity = 1 } = options; 15 | 16 | const material = new MeshBasicMaterial({ color, side: DoubleSide, transparent, opacity }); 17 | const geometry = new PlaneGeometry(width, height); 18 | 19 | this.setBody({ geometry, material }); 20 | this.setEntityType(ENTITY_TYPES.MESH); 21 | } 22 | 23 | static get UP() { 24 | return UP; 25 | } 26 | static get DOWN() { 27 | return DOWN; 28 | } 29 | 30 | face(direction) { 31 | const vector = new Vector3(direction.x, direction.y, direction.z); 32 | 33 | this.body.lookAt(vector); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/entities/base/Sphere.js: -------------------------------------------------------------------------------- 1 | import { SphereGeometry, MeshBasicMaterial } from "three"; 2 | import Element from "../Element"; 3 | import { ENTITY_TYPES } from "../constants"; 4 | import Color from "../../lib/Color"; 5 | 6 | export default class Sphere extends Element { 7 | constructor(radius = 10, color = Color.randomColor(true), options = {}) { 8 | super(options); 9 | 10 | const segments = 32; 11 | 12 | const geometry = new SphereGeometry(radius, segments, segments); 13 | const material = new MeshBasicMaterial({ 14 | color, 15 | wireframe: false, 16 | ...options, 17 | }); 18 | 19 | this.setBody({ geometry, material }); 20 | this.setEntityType(ENTITY_TYPES.MESH); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/entities/base/Sprite.js: -------------------------------------------------------------------------------- 1 | import Images from "../../images/Images"; 2 | import Scene from "../../core/Scene"; 3 | import { SpriteMaterial, Sprite as THREESprite } from "three"; 4 | import Element from "../Element"; 5 | import { ENTITY_TYPES } from "../constants"; 6 | 7 | const validateAnisotropy = anisotropy => { 8 | const max = Scene.getRenderer().capabilities.getMaxAnisotropy(); 9 | 10 | return anisotropy > max ? max : anisotropy; 11 | }; 12 | 13 | export default class Sprite extends Element { 14 | constructor(width = 20, height = 20, spriteTexture, options = {}) { 15 | super(options); 16 | 17 | const { anisotropy = 1, ...rest } = options; 18 | const texture = Images.get(spriteTexture); 19 | 20 | texture.anisotropy = validateAnisotropy(anisotropy); 21 | 22 | const material = new SpriteMaterial({ 23 | map: texture, 24 | ...rest, 25 | }); 26 | 27 | const body = new THREESprite(material); 28 | body.scale.x = width; 29 | body.scale.y = height; 30 | 31 | this.setBody({ body }); 32 | this.setEntityType(ENTITY_TYPES.SPRITE); 33 | } 34 | 35 | getRotation() { 36 | return this.body.material.rotation; 37 | } 38 | 39 | setRotation(rotation = this.getRotation()) { 40 | this.body.material.rotation = rotation; 41 | } 42 | 43 | setSizeAttenuation(attenuation = true) { 44 | this.body.material.sizeAttenuation = attenuation; 45 | } 46 | 47 | setDepthTest(depthTest = true) { 48 | this.body.material.depthTest = depthTest; 49 | } 50 | 51 | setDepthWrite(depthWrite = true) { 52 | this.body.material.depthWrite = depthWrite; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/entities/base/index.js: -------------------------------------------------------------------------------- 1 | import Grid from "./Grid"; 2 | import Axes from "./Axes"; 3 | import Cube from "./Cube"; 4 | import Sphere from "./Sphere"; 5 | import Cylinder from "./Cylinder"; 6 | import Line from "./Line"; 7 | import CurveLine from "./CurveLine"; 8 | import Plane from "./Plane"; 9 | import Box from "./Box"; 10 | import Sprite from "./Sprite"; 11 | import HelperSprite from "./HelperSprite"; 12 | import Label from "./Label"; 13 | import Cone from "./Cone"; 14 | 15 | export { 16 | Sphere, 17 | Grid, 18 | Cube, 19 | Cylinder, 20 | Line, 21 | CurveLine, 22 | Plane, 23 | Box, 24 | Sprite, 25 | HelperSprite, 26 | Axes, 27 | Label, 28 | Cone, 29 | }; 30 | -------------------------------------------------------------------------------- /src/entities/camera.js: -------------------------------------------------------------------------------- 1 | import { PerspectiveCamera, Vector3 } from "three"; 2 | import config from "../core/config"; 3 | import { ENTITY_TYPES } from "./constants"; 4 | import Entity from "./Entity"; 5 | 6 | export default class Camera extends Entity { 7 | constructor(options = {}) { 8 | const { 9 | name = "Main PerspectiveCamera", 10 | fov = config.camera().fov, 11 | ratio = config.screen().ratio, 12 | near = config.camera().near, 13 | far = config.camera().far, 14 | } = options; 15 | 16 | super({ name }); 17 | 18 | this.options = options; 19 | const body = new PerspectiveCamera(fov, ratio, near, far); 20 | 21 | this.setBody({ body }); 22 | this.setEntityType(ENTITY_TYPES.CAMERA); 23 | this.setName(name); 24 | } 25 | 26 | getPosition() { 27 | return this.body.position; 28 | } 29 | 30 | getDirection() { 31 | const vector = new Vector3(); 32 | const { x, y, z } = this.getBody().getWorldDirection(vector); 33 | 34 | return { 35 | x, 36 | y, 37 | z, 38 | }; 39 | } 40 | 41 | lookAt(position = {}) { 42 | const { x = 0, y = 0, z = 0 } = position; 43 | this.body.lookAt(x, y, z); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/entities/constants.js: -------------------------------------------------------------------------------- 1 | export const ENTITY_TYPES = { 2 | SCENE: 'SCENE', 3 | CAMERA: 'CAMERA', 4 | MESH: 'MESH', 5 | LABEL: 'LABEL', 6 | LIGHT: { 7 | DEFAULT: 'LIGHT.DEFAULT', 8 | AMBIENT: 'LIGHT.AMBIENT', 9 | SUN: 'LIGHT.SUN', 10 | HEMISPHERE: 'LIGHT.HEMISPHERE', 11 | POINT: 'LIGHT.POINT', 12 | SPOT: 'LIGHT.SPOT' 13 | }, 14 | AUDIO: { 15 | DEFAULT: 'AUDIO.DEFAULT', 16 | DIRECTIONAL: 'AUDIO.DIRECTIONAL', 17 | AMBIENT: 'AUDIO.AMBIENT' 18 | }, 19 | MODEL: 'MODEL', 20 | SPRITE: 'SPRITE', 21 | PARTICLE: 'PARTICLE', 22 | EFFECT: { 23 | PARTICLE: 'EFFECT.PARTICLE', 24 | SCENERY: 'EFFECT.SCENERY' 25 | }, 26 | HELPER: { 27 | GRID: 'HELPER.GRID', 28 | AXES: 'HELPER.AXES' 29 | }, 30 | UNKNOWN: 'UNKNOWN' 31 | }; 32 | 33 | export const FLAT_ENTITY_TYPES = [ 34 | ENTITY_TYPES.SCENE, 35 | ENTITY_TYPES.CAMERA, 36 | ENTITY_TYPES.MESH, 37 | ENTITY_TYPES.LABEL, 38 | ENTITY_TYPES.LIGHT.DEFAULT, 39 | ENTITY_TYPES.LIGHT.AMBIENT, 40 | ENTITY_TYPES.LIGHT.SUN, 41 | ENTITY_TYPES.LIGHT.HEMISPHERE, 42 | ENTITY_TYPES.LIGHT.POINT, 43 | ENTITY_TYPES.LIGHT.SPOT, 44 | ENTITY_TYPES.AUDIO.DEFAULT, 45 | ENTITY_TYPES.AUDIO.AMBIENT, 46 | ENTITY_TYPES.AUDIO.DIRECTIONAL, 47 | ENTITY_TYPES.AUDIO.BACKGROUND, 48 | ENTITY_TYPES.MODEL, 49 | ENTITY_TYPES.SPRITE, 50 | ENTITY_TYPES.PARTICLE, 51 | ENTITY_TYPES.EFFECT.PARTICLE, 52 | ENTITY_TYPES.EFFECT.SCENERY, 53 | ENTITY_TYPES.HELPER.GRID, 54 | ENTITY_TYPES.HELPER.AXES, 55 | ENTITY_TYPES.UNKNOWN 56 | ] 57 | 58 | export const ENTITY_EVENTS = { 59 | DISPOSE: 'DISPOSE', 60 | STATE_MACHINE: { 61 | CHANGE: 'STATE_MACHINE_CHANGE', 62 | }, 63 | ANIMATION: { 64 | LOOP: 'LOOP', 65 | FINISHED: 'FINISHED' 66 | }, 67 | AUDIO: { 68 | ENDED: 'ended' 69 | } 70 | }; 71 | 72 | export const DEFAULT_TAG = 'all'; -------------------------------------------------------------------------------- /src/entities/index.js: -------------------------------------------------------------------------------- 1 | import { ENTITY_TYPES, ENTITY_EVENTS } from "./constants"; 2 | import { 3 | Box, 4 | Cube, 5 | Cylinder, 6 | Grid, 7 | Line, 8 | CurveLine, 9 | Plane, 10 | Sphere, 11 | Sprite, 12 | HelperSprite, 13 | Axes, 14 | Label, 15 | Cone, 16 | } from "./base"; 17 | 18 | export { 19 | ENTITY_TYPES, 20 | ENTITY_EVENTS, 21 | Box, 22 | Cube, 23 | Cylinder, 24 | Grid, 25 | Line, 26 | CurveLine, 27 | Plane, 28 | Sprite, 29 | HelperSprite, 30 | Sphere, 31 | Axes, 32 | Label, 33 | Cone, 34 | }; 35 | -------------------------------------------------------------------------------- /src/fx/materials/Atmosphere.js: -------------------------------------------------------------------------------- 1 | import { 2 | FrontSide, 3 | AdditiveBlending, 4 | Color 5 | } from 'three'; 6 | 7 | export default class Atmosphere { 8 | 9 | constructor() { 10 | this.options = { 11 | side: FrontSide, 12 | blending: AdditiveBlending, 13 | transparent: true, 14 | depthWrite: false, 15 | }; 16 | 17 | this.attributes = {}; 18 | } 19 | 20 | static vertex() { 21 | return [ 22 | 'varying vec3 vNormal;', 23 | 'void main(){', 24 | ' // compute intensity', 25 | ' vNormal = normalize( normalMatrix * normal );', 26 | ' // set gl_Position', 27 | ' gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );', 28 | '}', 29 | ].join('\n'); 30 | } 31 | 32 | static fragment() { 33 | return [ 34 | 'uniform float coeficient;', 35 | 'uniform float power;', 36 | 'uniform vec3 glowColor;', 37 | 38 | 'varying vec3 vNormal;', 39 | 40 | 'void main(){', 41 | ' float intensity = pow( coeficient - dot(vNormal, vec3(0.0, 0.0, 1.0)), power );', 42 | ' gl_FragColor = vec4( glowColor * intensity, 1.0 );', 43 | '}', 44 | ].join('\n'); 45 | } 46 | 47 | static uniforms() { 48 | return { 49 | coeficient: { 50 | type: "f", 51 | value: 1.0 52 | }, 53 | power: { 54 | type: "f", 55 | value: 2 56 | }, 57 | glowColor: { 58 | type: "c", 59 | value: new Color('pink') 60 | }, 61 | }; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/fx/particles/Explosion.js: -------------------------------------------------------------------------------- 1 | import Proton from 'three.proton'; 2 | import ParticleEmitterGroup from './ParticleEmitterGroup'; 3 | import ProtonParticleEmitter from './ProtonParticleEmitter'; 4 | 5 | const DEFAULT_SIZE = 4; 6 | 7 | const getSparksInitializers = (size = DEFAULT_SIZE) => ([ 8 | new Proton.Mass(0.1), 9 | new Proton.Radius(size * .3), 10 | new Proton.Life(1), 11 | new Proton.Position(new Proton.SphereZone(size * 1.5)) 12 | ]); 13 | 14 | const getSparksBehaviours = (size = DEFAULT_SIZE) => ([ 15 | new Proton.RandomDrift(size * 0.75, size * 0.75, size * 0.75, .5), 16 | new Proton.Color('#ffffff'), 17 | new Proton.Scale(1, 0.1) 18 | ]); 19 | 20 | const getSparksRate = () => new Proton.Rate(50, .5); 21 | const getDebrisRate = () => new Proton.Rate(10, .4); 22 | const getFireRate = () => new Proton.Rate(10, .4); 23 | 24 | const getDebrisInitializers = (size = DEFAULT_SIZE) => ([ 25 | new Proton.Mass(10), 26 | new Proton.Radius(size * .25), 27 | new Proton.Life(2), 28 | new Proton.Position(new Proton.SphereZone(size * .75)) 29 | ]); 30 | 31 | const getDebrisBehaviours = (size = DEFAULT_SIZE) => { 32 | const zone = new Proton.BoxZone(0, 50, 0, 300, 100, 300) 33 | zone.friction = 0.95; 34 | zone.max = 7; 35 | return ([ 36 | new Proton.CrossZone(zone, "bound"), 37 | new Proton.Repulsion(new Proton.Vector3D(0, 0, 0), size * 12.5, size * 1.5), 38 | new Proton.G(3), 39 | new Proton.Color('#95a5a6', '#000000') 40 | ]) 41 | }; 42 | 43 | const getFireInitializers = (size = DEFAULT_SIZE) => ([ 44 | new Proton.Mass(1), 45 | new Proton.Radius(size * 1.5), 46 | new Proton.Life(.2, .5), 47 | new Proton.Position(new Proton.SphereZone(size * 1.25)) 48 | ]); 49 | 50 | const getFireBehaviours = () => ([ 51 | new Proton.Scale(1, 2), 52 | new Proton.Color('#c0392b', '#f1c40f') 53 | ]); 54 | export default class Explosion extends ParticleEmitterGroup { 55 | 56 | constructor(options = {}) { 57 | const { 58 | texture = false, 59 | hasDebris = false, 60 | size = DEFAULT_SIZE 61 | } = options; 62 | 63 | const sparks = new ProtonParticleEmitter({ rate: getSparksRate(), texture, initializers: getSparksInitializers(size), behaviours: getSparksBehaviours(size)}); 64 | const fire = new ProtonParticleEmitter({ rate: getFireRate(), texture, initializers: getFireInitializers(size), behaviours: getFireBehaviours(size) }); 65 | 66 | const system = [ 67 | sparks, 68 | fire 69 | ]; 70 | 71 | if (hasDebris) { 72 | system.push(new ProtonParticleEmitter({ rate: getDebrisRate(), texture, initializers: getDebrisInitializers(size), behaviours: getDebrisBehaviours(size) })) 73 | } 74 | 75 | const name = 'ExplosionGroup'; 76 | 77 | super({ system, name }); 78 | } 79 | } -------------------------------------------------------------------------------- /src/fx/particles/Fire.js: -------------------------------------------------------------------------------- 1 | import Proton from 'three.proton'; 2 | import { Vector3 } from 'three'; 3 | import ProtonParticleEmitter from "./ProtonParticleEmitter"; 4 | import PALETTES from '../../lib/palettes'; 5 | const getFireRate = () => new Proton.Rate(new Proton.Span(10, 15), new Proton.Span(.05, .1)); 6 | 7 | const getFireInitializers = (direction, strength, size) => ([ 8 | new Proton.Mass(1), 9 | new Proton.Life(1, 2), 10 | new Proton.Radius(size / 2, size / 1.5, 'center'), 11 | new Proton.Position(new Proton.SphereZone(size)), 12 | new Proton.V(new Proton.Span(strength, strength * 2), new Proton.Vector3D(direction.x, direction.y, direction.z), 5), //new Proton.Span(200, 500) 13 | ]); 14 | 15 | const getFireBehaviours = (direction, strength, colors = [PALETTES.FRENCH.MANDARIN_RED, PALETTES.FRENCH.MELON_MELODY]) => ([ 16 | new Proton.Scale(new Proton.Span(2, 2.5), 0), 17 | new Proton.G(strength / 100), 18 | new Proton.Color(colors[0], colors[1], Infinity, Proton.easeOutSine), 19 | new Proton.RandomDrift(direction.x / 100, direction.y / 100, direction.z / 100, 2.5) 20 | ]); 21 | 22 | export default class Fire extends ProtonParticleEmitter { 23 | 24 | constructor(options) { 25 | const { 26 | texture, 27 | direction = new Vector3(0, 1, 0), 28 | size = 20, 29 | strength = 100, 30 | colors, 31 | ...rest 32 | } = options; 33 | 34 | const fireOptions = { 35 | rate: getFireRate(), 36 | texture, 37 | initializers: getFireInitializers(direction, strength, size), 38 | behaviours: getFireBehaviours(direction, strength, colors), 39 | ...rest 40 | } 41 | 42 | super(fireOptions); 43 | } 44 | } -------------------------------------------------------------------------------- /src/fx/particles/Fountain.js: -------------------------------------------------------------------------------- 1 | import { Vector3 } from 'three'; 2 | import { Randomizers, Emitter } from 'mage-engine.particles'; 3 | 4 | import ParticleEmitter from './ParticleEmitter'; 5 | 6 | export default class Fountain extends ParticleEmitter { 7 | 8 | constructor({ container, autostart, particles, system }) { 9 | const options = { 10 | container, 11 | autostart, 12 | particles: { 13 | ttl: 10, 14 | globalSize: 5, 15 | velocity: new Randomizers.SphereRandomizer(12.5), 16 | velocityBonus: new Vector3(0, 25, 0), 17 | particlesCount: 1000, 18 | blending: 'blend', 19 | gravity: -10, 20 | startAlpha: 1, 21 | endAlpha: 0, 22 | startColor: new Randomizers.ColorsRandomizer(), 23 | endColor: new Randomizers.ColorsRandomizer(), 24 | startAlphaChangeAt: 0, 25 | onUpdate: (particle) => { 26 | let floorY = -10; 27 | if (particle.position.y < floorY) { 28 | particle.position.y = floorY; 29 | particle.velocity.y *= -0.5; 30 | particle.velocity.x *= 0.5; 31 | particle.velocity.z *= 0.5; 32 | } 33 | }, 34 | ...particles 35 | }, 36 | system: { 37 | particlesCount: 100, 38 | emitters: new Emitter({ 39 | onInterval: new Randomizers.MinMaxRandomizer(0, 5), 40 | interval: new Randomizers.MinMaxRandomizer(0, 0.25), 41 | }), 42 | speed: 1, 43 | ...system 44 | } 45 | }; 46 | 47 | super(options); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/fx/particles/Rain.js: -------------------------------------------------------------------------------- 1 | import { Vector3, Color } from 'three'; 2 | import { Randomizers, Emitter } from 'mage-engine.particles'; 3 | 4 | import ParticleEmitter from './ParticleEmitter'; 5 | 6 | export default class Rain extends ParticleEmitter { 7 | 8 | constructor({ container, autostart, particles, system }) { 9 | const options = { 10 | container, 11 | autostart, 12 | particles: { 13 | globalSize: 0.4, 14 | ttl: 5, 15 | velocity: new Vector3(0, -2, 0), 16 | velocityBonus: new Vector3(0, -60, 0), 17 | gravity: -10, 18 | startAlpha: 1, 19 | endAlpha: 0, 20 | startColor: new Color(1, 1, 1), 21 | endColor: new Color(0, 0, 0), 22 | startAlphaChangeAt: 0, 23 | blending: "additive", 24 | onSpawn: (particle) => { 25 | particle.position.x = Math.random() * 100; 26 | particle.position.z = Math.random() * 100; 27 | }, 28 | ...particles 29 | }, 30 | system: { 31 | particlesCount: 2000, 32 | emitters: new Emitter({ 33 | onInterval: new Randomizers.MinMaxRandomizer(0, 5), 34 | interval: new Randomizers.MinMaxRandomizer(0, 0.25), 35 | }), 36 | speed: 1.5, 37 | ...system 38 | } 39 | }; 40 | 41 | super(options); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/fx/particles/Snow.js: -------------------------------------------------------------------------------- 1 | import { Vector3, Color } from 'three'; 2 | import { Randomizers, Emitter } from 'mage-engine.particles'; 3 | 4 | import ParticleEmitter from './ParticleEmitter'; 5 | 6 | export default class Snow extends ParticleEmitter { 7 | 8 | constructor({ container, autostart, particles, system }) { 9 | const options = { 10 | container, 11 | autostart, 12 | particles: { 13 | globalSize: 0.7, 14 | ttl: 5, 15 | velocity: new Randomizers.SphereRandomizer(30), 16 | velocityBonus: new Vector3(0, -30, 0), 17 | gravity: -10, 18 | startAlpha: 0.8, 19 | endAlpha: 0, 20 | startColor: new Color(1, 1, 1), 21 | endColor: new Color(0, 0, 0), 22 | startAlphaChangeAt: 0, 23 | blending: "blend", 24 | onSpawn: (particle) => { 25 | particle.position.x = Math.random() * 200; 26 | particle.position.z = Math.random() * 200; 27 | }, 28 | ...particles 29 | }, 30 | system: { 31 | particlesCount: 4000, 32 | emitters: new Emitter({ 33 | onInterval: new Randomizers.MinMaxRandomizer(0, 5), 34 | interval: new Randomizers.MinMaxRandomizer(0, 0.25), 35 | }), 36 | speed: 1.5, 37 | ...system 38 | } 39 | }; 40 | 41 | super(options); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/fx/particles/Trail.js: -------------------------------------------------------------------------------- 1 | import Proton from 'three.proton'; 2 | import ParticleEmitterGroup from './ParticleEmitterGroup'; 3 | import ProtonParticleEmitter from './ProtonParticleEmitter'; 4 | 5 | const DEFAULT_SIZE = 4; 6 | 7 | const getTrailRate = () => new Proton.Rate( 8 | new Proton.Span(10, 20), 9 | new Proton.Span(0.01, 0.015) 10 | ); 11 | 12 | const getTrailInitialisers = (size) => ([ 13 | new Proton.Mass(1), 14 | new Proton.Life(1, 2), 15 | new Proton.Radius(size), 16 | ]); 17 | 18 | const getTrailBehaviour = () => ([ 19 | new Proton.Alpha(1, 0), 20 | new Proton.Color("#ffffff"), 21 | new Proton.Scale(.5, 0.1) 22 | ]) 23 | 24 | export default class Trail extends ParticleEmitterGroup { 25 | 26 | constructor(options = {}) { 27 | const { 28 | texture = false, 29 | size = DEFAULT_SIZE 30 | } = options; 31 | 32 | const system = [ 33 | new ProtonParticleEmitter({ 34 | rate: getTrailRate(), 35 | texture, 36 | initializers: getTrailInitialisers(size), 37 | behaviours: getTrailBehaviour(size) 38 | }) 39 | ]; 40 | 41 | const name = 'TrailGroup'; 42 | 43 | super({ system, name }); 44 | } 45 | } -------------------------------------------------------------------------------- /src/fx/particles/constants.js: -------------------------------------------------------------------------------- 1 | export const PARTICLE_EMITTER_TYPES = { 2 | SINGLE: 'SINGLE', 3 | GROUP: 'GROUP' 4 | }; -------------------------------------------------------------------------------- /src/fx/postprocessing/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MageStudio/Mage/b9d7e2283d3bde54b5c071be647be6530c6f701f/src/fx/postprocessing/.DS_Store -------------------------------------------------------------------------------- /src/fx/postprocessing/DepthOfField.js: -------------------------------------------------------------------------------- 1 | import Scene from '../../core/Scene'; 2 | import config from '../../core/config'; 3 | 4 | import BokehPass from './BokehPass'; 5 | 6 | export default class DepthOfField extends BokehPass { 7 | 8 | constructor(params) { 9 | const { w: width, h: height, ratio: aspect } = config.screen(); 10 | super( 11 | Scene.scene, 12 | Scene.getCameraBody(), 13 | { ...params, width, height, aspect }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/fx/postprocessing/HueSaturationEffect.js: -------------------------------------------------------------------------------- 1 | import ShaderPass from './passes/ShaderPass'; 2 | 3 | const HueSaturationShader = { 4 | 5 | uniforms: { 6 | "tDiffuse": { value: null }, 7 | "hue": { value: 0 }, 8 | "saturation": { value: 0 } 9 | }, 10 | 11 | vertexShader: [ 12 | 13 | "varying vec2 vUv;", 14 | 15 | "void main() {", 16 | 17 | "vUv = uv;", 18 | 19 | "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", 20 | 21 | "}" 22 | 23 | ].join( "\n" ), 24 | 25 | fragmentShader: [ 26 | 27 | "uniform sampler2D tDiffuse;", 28 | "uniform float hue;", 29 | "uniform float saturation;", 30 | 31 | "varying vec2 vUv;", 32 | 33 | "void main() {", 34 | 35 | "gl_FragColor = texture2D( tDiffuse, vUv );", 36 | 37 | // hue 38 | "float angle = hue * 3.14159265;", 39 | "float s = sin(angle), c = cos(angle);", 40 | "vec3 weights = (vec3(2.0 * c, -sqrt(3.0) * s - c, sqrt(3.0) * s - c) + 1.0) / 3.0;", 41 | "float len = length(gl_FragColor.rgb);", 42 | "gl_FragColor.rgb = vec3(", 43 | "dot(gl_FragColor.rgb, weights.xyz),", 44 | "dot(gl_FragColor.rgb, weights.zxy),", 45 | "dot(gl_FragColor.rgb, weights.yzx)", 46 | ");", 47 | 48 | // saturation 49 | "float average = (gl_FragColor.r + gl_FragColor.g + gl_FragColor.b) / 3.0;", 50 | "if (saturation > 0.0) {", 51 | "gl_FragColor.rgb += (average - gl_FragColor.rgb) * (1.0 - 1.0 / (1.001 - saturation));", 52 | "} else {", 53 | "gl_FragColor.rgb += (average - gl_FragColor.rgb) * (-saturation);", 54 | "}", 55 | 56 | "}" 57 | 58 | ].join( "\n" ) 59 | 60 | }; 61 | 62 | export default ({ hue = 0, saturation = 0, renderToScreen = false }) => { 63 | const shader = { 64 | ...HueSaturationShader 65 | }; 66 | 67 | shader.uniforms.hue.value = hue; 68 | shader.uniforms.saturation.value = saturation; 69 | 70 | const pass = new ShaderPass(shader); 71 | pass.renderToScreen = renderToScreen; 72 | 73 | return pass; 74 | } 75 | -------------------------------------------------------------------------------- /src/fx/postprocessing/PixelEffect.js: -------------------------------------------------------------------------------- 1 | import { 2 | Vector2 3 | } from 'three'; 4 | import ShaderPass from './passes/ShaderPass'; 5 | import Config from '../../core/config'; 6 | 7 | const PixelShader = { 8 | 9 | uniforms: { 10 | 11 | "tDiffuse": { value: null }, 12 | "resolution": { value: null }, 13 | "pixelSize": { value: 1. }, 14 | 15 | }, 16 | 17 | vertexShader: [ 18 | 19 | "varying highp vec2 vUv;", 20 | 21 | "void main() {", 22 | 23 | "vUv = uv;", 24 | "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", 25 | 26 | "}" 27 | 28 | ].join( "\n" ), 29 | 30 | fragmentShader: [ 31 | 32 | "uniform sampler2D tDiffuse;", 33 | "uniform float pixelSize;", 34 | "uniform vec2 resolution;", 35 | 36 | "varying highp vec2 vUv;", 37 | 38 | "void main(){", 39 | 40 | "vec2 dxy = pixelSize / resolution;", 41 | "vec2 coord = dxy * floor( vUv / dxy );", 42 | "gl_FragColor = texture2D(tDiffuse, coord);", 43 | 44 | "}" 45 | 46 | ].join( "\n" ) 47 | }; 48 | 49 | export default class PixelEffect extends ShaderPass { 50 | 51 | constructor({ pixelSize = 16, renderToScreen = false }) { 52 | const { h, w, devicePixelRatio } = Config.screen(); 53 | 54 | super(PixelShader); 55 | 56 | this.renderToScreen = renderToScreen; 57 | 58 | this.uniforms["pixelSize"].value = pixelSize; 59 | this.uniforms["resolution"].value = new Vector2(w, h); 60 | this.uniforms["resolution"].value.multiplyScalar(devicePixelRatio); 61 | } 62 | 63 | setPixelSize(pixelSize = 16) { 64 | this.uniforms["pixelSize"].value = pixelSize; 65 | } 66 | 67 | onWindowResize() { 68 | const { h, w, devicePixelRatio } = Config.screen(); 69 | 70 | this.uniforms["resolution"].value.set(w, h).multiplyScalar(devicePixelRatio); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/fx/postprocessing/SepiaEffect.js: -------------------------------------------------------------------------------- 1 | import ShaderPass from './passes/ShaderPass'; 2 | 3 | const SepiaShader = { 4 | uniforms: { 5 | "tDiffuse": { value: null }, 6 | "amount": { value: 1.0 } 7 | }, 8 | vertexShader: [ 9 | "varying vec2 vUv;", 10 | "void main() {", 11 | "vUv = uv;", 12 | "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", 13 | "}" 14 | ].join( "\n" ), 15 | fragmentShader: [ 16 | "uniform float amount;", 17 | "uniform sampler2D tDiffuse;", 18 | "varying vec2 vUv;", 19 | "void main() {", 20 | "vec4 color = texture2D( tDiffuse, vUv );", 21 | "vec3 c = color.rgb;", 22 | "color.r = dot( c, vec3( 1.0 - 0.607 * amount, 0.769 * amount, 0.189 * amount ) );", 23 | "color.g = dot( c, vec3( 0.349 * amount, 1.0 - 0.314 * amount, 0.168 * amount ) );", 24 | "color.b = dot( c, vec3( 0.272 * amount, 0.534 * amount, 1.0 - 0.869 * amount ) );", 25 | "gl_FragColor = vec4( min( vec3( 1.0 ), color.rgb ), color.a );", 26 | "}" 27 | ].join( "\n" ) 28 | } 29 | 30 | export default ({ value = 1.0, renderToScreen = false }) => { 31 | const shader = SepiaShader; 32 | shader.uniforms.amount.value = value; 33 | 34 | const pass = new ShaderPass(shader); 35 | pass.renderToScreen = renderToScreen; 36 | 37 | return pass; 38 | } 39 | -------------------------------------------------------------------------------- /src/fx/postprocessing/passes/ClearMaskPass.js: -------------------------------------------------------------------------------- 1 | import Pass from './Pass'; 2 | 3 | export default class ClearMaskPass extends Pass { 4 | 5 | constructor() { 6 | super(); 7 | this.needsSwap = false; 8 | } 9 | 10 | render(renderer /*, writeBuffer, readBuffer, deltaTime, maskActive */) { 11 | renderer.state.buffers.stencil.setLocked(false); 12 | renderer.state.buffers.stencil.setTest(false); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/fx/postprocessing/passes/MaskPass.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @author alteredq / http://alteredqualia.com/ 4 | */ 5 | 6 | import Pass from "./Pass.js"; 7 | 8 | export default class MaskPass extends Pass { 9 | 10 | constructor(scene, camera) { 11 | super(); 12 | 13 | this.scene = scene; 14 | this.camera = camera; 15 | 16 | this.clear = true; 17 | this.needsSwap = false; 18 | 19 | this.inverse = false; 20 | } 21 | 22 | render(renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */) { 23 | 24 | const context = renderer.getContext(); 25 | const state = renderer.state; 26 | 27 | // don't update color or depth 28 | 29 | state.buffers.color.setMask(false); 30 | state.buffers.depth.setMask(false); 31 | 32 | // lock buffers 33 | 34 | state.buffers.color.setLocked(true); 35 | state.buffers.depth.setLocked(true); 36 | 37 | // set up stencil 38 | 39 | let writeValue, clearValue; 40 | 41 | if (this.inverse) { 42 | writeValue = 0; 43 | clearValue = 1; 44 | } else { 45 | writeValue = 1; 46 | clearValue = 0; 47 | } 48 | 49 | state.buffers.stencil.setTest(true); 50 | state.buffers.stencil.setOp(context.REPLACE, context.REPLACE, context.REPLACE); 51 | state.buffers.stencil.setFunc(context.ALWAYS, writeValue, 0xffffffff); 52 | state.buffers.stencil.setClear(clearValue); 53 | state.buffers.stencil.setLocked(true); 54 | 55 | // draw into the stencil buffer 56 | 57 | renderer.setRenderTarget(readBuffer); 58 | if (this.clear) renderer.clear(); 59 | renderer.render(this.scene, this.camera); 60 | 61 | renderer.setRenderTarget(writeBuffer); 62 | if (this.clear) renderer.clear(); 63 | renderer.render(this.scene, this.camera); 64 | 65 | // unlock color and depth buffer for subsequent rendering 66 | 67 | state.buffers.color.setLocked(false); 68 | state.buffers.depth.setLocked(false); 69 | 70 | // only render where stencil is set to 1 71 | 72 | state.buffers.stencil.setLocked(false); 73 | state.buffers.stencil.setFunc(context.EQUAL, 1, 0xffffffff); // draw if == 1 74 | state.buffers.stencil.setOp(context.KEEP, context.KEEP, context.KEEP); 75 | state.buffers.stencil.setLocked(true); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/fx/postprocessing/passes/Pass.js: -------------------------------------------------------------------------------- 1 | import { 2 | Mesh, 3 | OrthographicCamera, 4 | BufferGeometry, 5 | Float32BufferAttribute 6 | } from 'three'; 7 | 8 | export default class Pass { 9 | 10 | constructor() { 11 | // if set to true, the pass is processed by the composer 12 | this.enabled = true; 13 | 14 | // if set to true, the pass indicates to swap read and write buffer after rendering 15 | this.needsSwap = true; 16 | 17 | // if set to true, the pass clears its buffer before rendering 18 | this.clear = false; 19 | 20 | // if set to true, the result of the pass is rendered to screen. This is set automatically by EffectComposer. 21 | this.renderToScreen = false; 22 | 23 | this.isPass = true; 24 | } 25 | 26 | setSize( /* width, height */ ) {} 27 | 28 | render( /* renderer, writeBuffer, readBuffer, deltaTime, maskActive */ ) { 29 | console.error( 'THREE.Pass: .render() must be implemented in derived pass.' ); 30 | } 31 | 32 | } 33 | 34 | // Helper for passes that need to fill the viewport with a single quad. 35 | 36 | const _camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 ); 37 | 38 | // https://github.com/mrdoob/three.js/pull/21358 39 | 40 | const _geometry = new BufferGeometry(); 41 | _geometry.setAttribute( 'position', new Float32BufferAttribute( [ - 1, 3, 0, - 1, - 1, 0, 3, - 1, 0 ], 3 ) ); 42 | _geometry.setAttribute( 'uv', new Float32BufferAttribute( [ 0, 2, 0, 0, 2, 0 ], 2 ) ); 43 | 44 | export class FullScreenQuad { 45 | 46 | constructor( material ) { 47 | this._mesh = new Mesh( _geometry, material ); 48 | } 49 | 50 | dispose() { 51 | this._mesh.geometry.dispose(); 52 | } 53 | 54 | render( renderer ) { 55 | renderer.render( this._mesh, _camera ); 56 | } 57 | 58 | get material() { 59 | return this._mesh.material; 60 | } 61 | 62 | set material( value ) { 63 | this._mesh.material = value; 64 | } 65 | } -------------------------------------------------------------------------------- /src/fx/postprocessing/passes/RenderPass.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author alteredq / http://alteredqualia.com/ 3 | */ 4 | import { Color } from 'three'; 5 | import Pass from "./Pass.js"; 6 | 7 | export default class RenderPass extends Pass { 8 | 9 | constructor(scene, camera, overrideMaterial, clearColor, clearAlpha) { 10 | super(); 11 | 12 | this.scene = scene; 13 | this.camera = camera; 14 | 15 | this.overrideMaterial = overrideMaterial; 16 | 17 | this.clearColor = clearColor; 18 | this.clearAlpha = (clearAlpha !== undefined) ? clearAlpha : 0; 19 | 20 | this.clear = true; 21 | this.clearDepth = false; 22 | this.needsSwap = false; 23 | this._oldClearColor = new Color(); 24 | } 25 | 26 | render(renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */) { 27 | 28 | const oldAutoClear = renderer.autoClear; 29 | renderer.autoClear = false; 30 | 31 | let oldClearAlpha, oldOverrideMaterial; 32 | 33 | if (this.overrideMaterial !== undefined) { 34 | oldOverrideMaterial = this.scene.overrideMaterial; 35 | this.scene.overrideMaterial = this.overrideMaterial; 36 | } 37 | 38 | if (this.clearColor) { 39 | renderer.getClearColor(this._oldClearColor); 40 | oldClearAlpha = renderer.getClearAlpha(); 41 | 42 | renderer.setClearColor(this.clearColor, this.clearAlpha); 43 | } 44 | 45 | if (this.clearDepth) { 46 | renderer.clearDepth(); 47 | } 48 | 49 | renderer.setRenderTarget(this.renderToScreen ? null : readBuffer); 50 | 51 | // TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600 52 | if (this.clear) renderer.clear(renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil); 53 | renderer.render(this.scene, this.camera); 54 | 55 | if (this.clearColor) { 56 | renderer.setClearColor(this._oldClearColor, oldClearAlpha); 57 | } 58 | 59 | if (this.overrideMaterial !== undefined) { 60 | this.scene.overrideMaterial = oldOverrideMaterial; 61 | } 62 | 63 | renderer.autoClear = oldAutoClear; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/fx/postprocessing/passes/ShaderPass.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author alteredq / http://alteredqualia.com/ 3 | */ 4 | 5 | import { 6 | ShaderMaterial, 7 | UniformsUtils 8 | } from "three"; 9 | import Pass, { FullScreenQuad } from "./Pass"; 10 | 11 | export default class ShaderPass extends Pass { 12 | 13 | constructor(shader, textureID) { 14 | 15 | super(); 16 | 17 | this.textureID = (textureID !== undefined) ? textureID : 'tDiffuse'; 18 | 19 | if (shader instanceof ShaderMaterial) { 20 | this.uniforms = shader.uniforms; 21 | this.material = shader; 22 | } else if (shader) { 23 | this.uniforms = UniformsUtils.clone(shader.uniforms); 24 | 25 | this.material = new ShaderMaterial({ 26 | defines: Object.assign({}, shader.defines), 27 | uniforms: this.uniforms, 28 | vertexShader: shader.vertexShader, 29 | fragmentShader: shader.fragmentShader 30 | }); 31 | } 32 | 33 | this.fsQuad = new FullScreenQuad(this.material); 34 | } 35 | 36 | render(renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */) { 37 | 38 | if (this.uniforms[ this.textureID ]) { 39 | this.uniforms[ this.textureID ].value = readBuffer.texture; 40 | } 41 | 42 | this.fsQuad.material = this.material; 43 | 44 | if (this.renderToScreen) { 45 | renderer.setRenderTarget(null); 46 | this.fsQuad.render(renderer); 47 | } else { 48 | renderer.setRenderTarget(writeBuffer); 49 | // TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600 50 | if (this.clear) renderer.clear(renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil); 51 | this.fsQuad.render(renderer); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/fx/postprocessing/shaders/ConvolutionShader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author alteredq / http://alteredqualia.com/ 3 | * 4 | * Convolution shader 5 | * ported from o3d sample to WebGL / GLSL 6 | * http://o3d.googlecode.com/svn/trunk/samples/convolution.html 7 | */ 8 | 9 | import { 10 | Vector2 11 | } from "three"; 12 | 13 | const ConvolutionShader = { 14 | 15 | defines: { 16 | "KERNEL_SIZE_FLOAT": "25.0", 17 | "KERNEL_SIZE_INT": "25" 18 | }, 19 | 20 | uniforms: { 21 | "tDiffuse": { value: null }, 22 | "uImageIncrement": { value: new Vector2(0.001953125, 0.0) }, 23 | "cKernel": { value: [] } 24 | }, 25 | 26 | vertexShader: [ 27 | 28 | "uniform vec2 uImageIncrement;", 29 | 30 | "varying vec2 vUv;", 31 | 32 | "void main() {", 33 | 34 | " vUv = uv - ((KERNEL_SIZE_FLOAT - 1.0) / 2.0) * uImageIncrement;", 35 | " gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);", 36 | 37 | "}" 38 | 39 | ].join("\n"), 40 | 41 | fragmentShader: [ 42 | 43 | "uniform float cKernel[KERNEL_SIZE_INT];", 44 | 45 | "uniform sampler2D tDiffuse;", 46 | "uniform vec2 uImageIncrement;", 47 | 48 | "varying vec2 vUv;", 49 | 50 | "void main() {", 51 | 52 | " vec2 imageCoord = vUv;", 53 | " vec4 sum = vec4(0.0, 0.0, 0.0, 0.0);", 54 | 55 | " for(int i = 0; i < KERNEL_SIZE_INT; i ++) {", 56 | 57 | " sum += texture2D(tDiffuse, imageCoord) * cKernel[i];", 58 | " imageCoord += uImageIncrement;", 59 | 60 | " }", 61 | 62 | " gl_FragColor = sum;", 63 | 64 | "}" 65 | 66 | 67 | ].join("\n"), 68 | 69 | buildKernel: function (sigma) { 70 | 71 | // We lop off the sqrt(2 * pi) * sigma term, since we're going to normalize anyway. 72 | 73 | function gauss(x, sigma) { 74 | return Math.exp(- (x * x) / (2.0 * sigma * sigma)); 75 | } 76 | 77 | let i, values, sum, halfWidth, kMaxKernelSize = 25, kernelSize = 2 * Math.ceil(sigma * 3.0) + 1; 78 | 79 | if (kernelSize > kMaxKernelSize) kernelSize = kMaxKernelSize; 80 | halfWidth = (kernelSize - 1) * 0.5; 81 | 82 | values = new Array(kernelSize); 83 | sum = 0.0; 84 | for (i = 0; i < kernelSize; ++ i) { 85 | 86 | values[i] = gauss(i - halfWidth, sigma); 87 | sum += values[i]; 88 | 89 | } 90 | 91 | // normalize the kernel 92 | 93 | for (i = 0; i < kernelSize; ++ i) values[i] /= sum; 94 | 95 | return values; 96 | 97 | } 98 | 99 | }; 100 | 101 | export default ConvolutionShader; 102 | -------------------------------------------------------------------------------- /src/fx/postprocessing/shaders/CopyShader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Full-screen textured quad shader 3 | */ 4 | 5 | const CopyShader = { 6 | 7 | uniforms: { 8 | 'tDiffuse': { value: null }, 9 | 'opacity': { value: 1.0 } 10 | }, 11 | 12 | vertexShader: /* glsl */` 13 | 14 | varying vec2 vUv; 15 | 16 | void main() { 17 | vUv = uv; 18 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 19 | }`, 20 | 21 | fragmentShader: /* glsl */` 22 | 23 | uniform float opacity; 24 | 25 | uniform sampler2D tDiffuse; 26 | 27 | varying vec2 vUv; 28 | 29 | void main() { 30 | gl_FragColor = texture2D( tDiffuse, vUv ); 31 | gl_FragColor.a *= opacity; 32 | }` 33 | 34 | }; 35 | 36 | export default CopyShader; -------------------------------------------------------------------------------- /src/fx/scenery/Skybox.js: -------------------------------------------------------------------------------- 1 | import { 2 | CubeTexture, 3 | RGBFormat, 4 | MeshBasicMaterial, 5 | BoxGeometry, 6 | BackSide 7 | } from 'three'; 8 | 9 | import { generateRandomName, generateUUID } from '../../lib/uuid'; 10 | import Images from '../../images/Images'; 11 | import Element from '../../entities/Element'; 12 | 13 | export default class Skybox extends Element { 14 | 15 | constructor(options) { 16 | const { 17 | name = generateRandomName('Skybox'), 18 | texture = 'skybox', 19 | ...rest 20 | } = options; 21 | 22 | super({ name, texture, ...rest }); 23 | 24 | this.cubeMap = typeof texture === 'string' ? Images.get(texture) : texture; 25 | 26 | const material = new MeshBasicMaterial({ 27 | envMap: this.cubeMap, 28 | side: BackSide 29 | }); 30 | const geometry = new BoxGeometry( 1000000, 1000000, 1000000 ); 31 | 32 | this.setBody({ material, geometry }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/fx/shaders/Shaders.js: -------------------------------------------------------------------------------- 1 | import Atmosphere from '../materials/Atmosphere'; 2 | import Mirror from '../materials/Mirror'; 3 | import Ocean from '../materials/Ocean'; 4 | import Water from '../materials/Water'; 5 | import OceanShaders from '../materials/OceanShaders'; 6 | import Skybox from '../scenery/Skybox'; 7 | import Sky from '../scenery/Sky'; 8 | 9 | export class Shaders { 10 | 11 | constructor() { 12 | this.map = { 13 | Atmosphere, 14 | Mirror, 15 | Ocean, 16 | OceanShaders, 17 | Skybox, 18 | Sky, 19 | Water 20 | }; 21 | 22 | this.shaders = {}; 23 | 24 | this.numShaders = 0; 25 | this.shadersLoaded = 0; 26 | } 27 | 28 | get = (id) => { 29 | return this.map[id] || false; 30 | } 31 | 32 | create(name, params) { 33 | this.map[name] = { 34 | name, 35 | vertex: params.vertex || "", 36 | fragment: params.fragment || "", 37 | options: params.options || {}, 38 | attributes: params.attributes || {}, 39 | uniforms: params.uniforms || {}, 40 | instance: params.instance || false 41 | }; 42 | } 43 | 44 | add(name, shader) { 45 | this.map[name] = shader; 46 | } 47 | } 48 | 49 | export default new Shaders(); 50 | -------------------------------------------------------------------------------- /src/fx/shaders/shader.js: -------------------------------------------------------------------------------- 1 | import { ShaderMaterial } from 'three'; 2 | import Shaders from './Shaders'; 3 | 4 | export default class Shader { 5 | 6 | constructor( name, attributes, uniforms, options ) { 7 | this.instance = Shaders.get(name); 8 | 9 | if (!this.instance) { 10 | this.name = this.shader.name; 11 | this.vertex = this.shader.vertex; 12 | this.fragment = this.shader.fragment; 13 | this.attributes = attributes ? attributes : this.shader.attributes; 14 | this.uniforms = uniforms ? uniforms : this.shader.uniforms; 15 | 16 | //creating shader options 17 | var object = { 18 | 'attributes': this.attributes, 19 | 'uniforms': this.uniforms, 20 | 'vertexShader': this.shader.vertex, 21 | 'fragmentShader': this.shader.fragment 22 | }; 23 | 24 | //storing user options in shader options 25 | var opt = options ? options : this.shader.options; 26 | for (o in opt) { 27 | object[o] = opt[o]; 28 | } 29 | 30 | //creating the actual material 31 | this.material = new ShaderMaterial(object); 32 | } else { 33 | this.instance = this.shader.instance; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/Color.js: -------------------------------------------------------------------------------- 1 | import { Color as THREEColor } from "three"; 2 | 3 | export default class Color { 4 | static randomColor(asNumber = false) { 5 | const letters = "0123456789ABCDEF".split(""); 6 | let color = ""; 7 | for (let i = 0; i < 6; i++) { 8 | color += letters[Math.floor(Math.random() * 16)]; 9 | } 10 | return asNumber ? Number(`0x${color}`) : `#${color}`; 11 | } 12 | 13 | static componentToHex(c) { 14 | const hex = c.toString(16); 15 | return hex.length == 1 ? "0" + hex : hex; 16 | } 17 | 18 | static gbToHex(r, g, b) { 19 | return "0x" + Color.componentToHex(r) + Color.componentToHex(g) + Color.componentToHex(b); 20 | } 21 | 22 | static getIntValueFromHex(hex) { 23 | return parseInt(hex, 16); 24 | } 25 | 26 | constructor(color) { 27 | this.color = new THREEColor(color); 28 | } 29 | 30 | getColor() { 31 | return this.color; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/lib/colliders.js: -------------------------------------------------------------------------------- 1 | import { 2 | Vector3, 3 | Raycaster 4 | } from 'three'; 5 | import Scene from '../core/Scene'; 6 | import { Line } from '../entities'; 7 | 8 | export const DEFAULT_COLLIDER_OFFSET = { x: 0, y: 0, z: 0 }; 9 | export const DEFAULT_NEAR = 0; 10 | export const DEFAULT_FAR = 10; 11 | 12 | export const getPointsFromRayCollider = (ray, position) => { 13 | const origin = position.clone(); 14 | const end = origin.add(ray.ray.direction.clone().multiplyScalar(ray.far)); 15 | 16 | return [origin, end]; 17 | }; 18 | 19 | export const createColliderHelper = (ray, position) => { 20 | return new Line(getPointsFromRayCollider(ray, position)); 21 | }; 22 | 23 | export const parseColliderFromDescription = ({ type, vector }, options, mesh, isSprite = false) => { 24 | const { near = DEFAULT_NEAR, far = DEFAULT_FAR, offset = DEFAULT_COLLIDER_OFFSET, debug = false } = options; 25 | const parsedOffset = { 26 | ...DEFAULT_COLLIDER_OFFSET, 27 | ...offset 28 | }; 29 | const position = mesh.position 30 | .clone() 31 | .add(new Vector3(parsedOffset.x, parsedOffset.y, parsedOffset.z)); 32 | const ray = new Raycaster(position, vector, near, far); 33 | const helper = debug && createColliderHelper(ray, position); 34 | 35 | if (isSprite) { 36 | ray.setFromCamera(position, Scene.getCameraBody()); 37 | } 38 | 39 | return { 40 | type, 41 | ray, 42 | helper, 43 | offset: parsedOffset 44 | }; 45 | }; -------------------------------------------------------------------------------- /src/lib/dom.js: -------------------------------------------------------------------------------- 1 | import { removeFirst } from "./strings"; 2 | 3 | export const isClassname = word => word.startsWith('.'); 4 | 5 | export const isId = word => word.startsWith('#'); 6 | 7 | export const DEFAULT_FULL_SIZE_STYLE = { 8 | position: 'absolute', 9 | height: '100%', 10 | width: '100%', 11 | margin: '0' 12 | }; 13 | 14 | export const getOrCreateElement = (selector, type, style) => ( 15 | document.querySelector(selector) || 16 | createElementFromSelector(selector, type, style) 17 | ) 18 | 19 | export const removeElement = selector => ( 20 | document.querySelector(selector).remove() 21 | ) 22 | 23 | export const createElementFromSelector = (selector, type = 'div', style = DEFAULT_FULL_SIZE_STYLE) => { 24 | const element = document.createElement(type); 25 | 26 | Object 27 | .keys(style) 28 | .forEach((property) => { 29 | element.style.setProperty(property, style[property]); 30 | }); 31 | 32 | if (isClassname(selector)) { 33 | element.className = removeFirst(selector); 34 | } else if (isId(selector)) { 35 | element.id = removeFirst(selector); 36 | } 37 | 38 | return element; 39 | }; 40 | -------------------------------------------------------------------------------- /src/lib/easing.js: -------------------------------------------------------------------------------- 1 | import between from 'between.js'; 2 | 3 | export const LOOPING = { 4 | BOUNCE: 'bounce', 5 | REPEAT: 'repeat', 6 | NONE: false 7 | }; 8 | 9 | export const EASING_EVENTS = { 10 | UPDATE: 'update', 11 | COMPLETE: 'complete' 12 | }; 13 | 14 | export const FUNCTIONS = { 15 | ...between.Easing 16 | }; 17 | 18 | export const tweenTo = (origin, target, options = {}) => ( 19 | new Promise((resolve) => { 20 | const { 21 | time, 22 | easing = FUNCTIONS.Linear.None, 23 | loop = LOOPING.NONE, 24 | onUpdate = f => f, 25 | repeat = undefined 26 | } = options; 27 | 28 | const tween = new between(origin, target) 29 | .time(time) 30 | .easing(easing) 31 | .on(EASING_EVENTS.UPDATE, onUpdate) 32 | 33 | const infinite = loop && !repeat; 34 | const onComplete = () => resolve(tween, infinite); 35 | 36 | if (loop) { 37 | tween.loop(loop, repeat); 38 | } 39 | 40 | if (infinite) { 41 | const timeToCompleteLoop = time * 2; 42 | setTimeout(onComplete, timeToCompleteLoop); 43 | } else { 44 | tween.on(EASING_EVENTS.COMPLETE, onComplete); 45 | } 46 | }) 47 | ); 48 | 49 | -------------------------------------------------------------------------------- /src/lib/functions.js: -------------------------------------------------------------------------------- 1 | export const debounce = (func, wait, immediate, context = this) =>{ 2 | let timeout; 3 | return function() { 4 | const args = arguments; 5 | const later = function() { 6 | timeout = null; 7 | if (!immediate) func.apply(context, args); 8 | }; 9 | const callNow = immediate && !timeout; 10 | clearTimeout(timeout); 11 | timeout = setTimeout(later, wait); 12 | if (callNow) func.apply(context, args); 13 | }; 14 | }; 15 | 16 | export const NOOP = f => f; -------------------------------------------------------------------------------- /src/lib/location.js: -------------------------------------------------------------------------------- 1 | import { HASH } from "./constants"; 2 | 3 | export const hasLocation = () => window && window.location; 4 | 5 | export const getLocationHash = () => ( 6 | hasLocation() ? 7 | location.hash : 8 | HASH 9 | ); 10 | 11 | export const setLocationHash = (hash = '', options = '') => { 12 | if (hasLocation()) { 13 | location.hash =`${hash}${options}`; 14 | } 15 | } -------------------------------------------------------------------------------- /src/lib/map.js: -------------------------------------------------------------------------------- 1 | export const serializeMap = map => { 2 | const reducer = m => 3 | [...m].reduce((a, v) => { 4 | a[v[0]] = v[1]; 5 | return a; 6 | }, {}); 7 | const replacer = (key, value) => (value instanceof Map ? reducer(value) : value); 8 | 9 | return JSON.stringify(map, replacer); 10 | }; 11 | 12 | export const deserialiseMap = o => { 13 | const json = JSON.parse(o); 14 | const m = new Map(); 15 | 16 | Object.keys(json).forEach(k => { 17 | m.set(k, json[k]); 18 | }); 19 | 20 | return m; 21 | }; 22 | 23 | export const populateMap = (map, data) => { 24 | Object.keys(data).forEach(k => { 25 | map.set(k, data[k]); 26 | }); 27 | 28 | return map; 29 | }; 30 | -------------------------------------------------------------------------------- /src/lib/network.js: -------------------------------------------------------------------------------- 1 | import { dispatch } from "../store/Store"; 2 | import { networkChanged } from "../store/actions/network"; 3 | 4 | const ONLINE_EVENT = 'online'; 5 | const OFFLINE_EVENT = 'offline'; 6 | 7 | const handleOnline = () => dispatch(networkChanged(true)); 8 | const handleOffline = () => dispatch(networkChanged(false)); 9 | 10 | export const listenToNetworkChanges = () => { 11 | window.addEventListener(ONLINE_EVENT, handleOnline); 12 | window.addEventListener(OFFLINE_EVENT, handleOffline); 13 | }; 14 | 15 | export const stopListeningToNetworkChanges = () => { 16 | window.removeEventListener(ONLINE_EVENT, handleOnline); 17 | window.removeEventListener(OFFLINE_EVENT, handleOffline); 18 | }; -------------------------------------------------------------------------------- /src/lib/palettes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * THANKS TO https://flatuicolors.com/ for providing the palettes below. 3 | */ 4 | 5 | const BASE = { 6 | WHITE: 0XFFFFFF, 7 | BLACK: 0x000000, 8 | RED: 0xFF0000, 9 | GREEN: 0x00FF00, 10 | BLUE: 0x0000FF, 11 | YELLOW: 0xFFFF00, 12 | CYAN: 0x00FFFF, 13 | MAGENTA: 0xFF00FF, 14 | SILVER: 0xC0C0C0, 15 | GRAY: 0x808080, 16 | MAROON: 0x800000, 17 | OLIVE: 0x808000, 18 | PURPLE: 0x800080, 19 | TEAL: 0x008080, 20 | NAVY: 0x000080, 21 | }; 22 | 23 | 24 | const FRENCH_PALETTE = { 25 | /** 26 | * french palette by Lea Poisson https://bit.ly/3iEo73B from 🇫🇷 27 | */ 28 | 29 | FLAT_FLESH: 0Xfad390, 30 | SQUASH_BLOSSOM: 0Xf6b93b, 31 | ICELAND_POPPY: 0Xfa983a, 32 | CARROT_ORANGE: 0Xe58e26, 33 | 34 | MELON_MELODY: 0Xf8c291, 35 | MANDARIN_RED: 0Xe55039, 36 | TOMATO_RED: 0Xeb2f06, 37 | JALAPENO_RED: 0Xb71540, 38 | 39 | LIVID: 0X6a89cc, 40 | AZRAQ_BLUE: 0X4a69bd, 41 | YUE_GUANG_LAN_BLUE: 0X1e3799, 42 | DARK_SAPPHIRE: 0X0c2461, 43 | 44 | SPRAY: 0X82ccdd, 45 | DUPAIN: 0X60a3bc, 46 | GOOD_SAMARITAN: 0X3c6382, 47 | FOREST_BLUE: 0X0a3d62, 48 | 49 | PARADISE_GREEN: 0Xb8e994, 50 | AURORA_GREEN: 0X78e08f, 51 | WATERFALL: 0X38ada9, 52 | REEF_ENCOUNTER: 0X079992 53 | }; 54 | 55 | export default { 56 | BASE, 57 | FRENCH_PALETTE, 58 | FRENCH: FRENCH_PALETTE, 59 | }; -------------------------------------------------------------------------------- /src/lib/query.js: -------------------------------------------------------------------------------- 1 | const QUERY_PREFIX = '?'; 2 | const AMPERSAND = '&'; 3 | const EQUALS = '='; 4 | const EMPTY = ''; 5 | const EMPTY_QUERY = {}; 6 | 7 | export const toQueryString = (query = {}) => { 8 | const keys = Object.keys(query); 9 | if (keys.length == 0) { 10 | return EMPTY; 11 | } 12 | 13 | const q = QUERY_PREFIX + (keys.map(k => k + EQUALS + query[k] + AMPERSAND).join(EMPTY)); 14 | 15 | return q.endsWith(AMPERSAND) ? q.substr(0, q.length -1) : q; 16 | }; 17 | 18 | export const parseQuery = (queryString = '') => { 19 | return queryString.length > 1 ? 20 | queryString 21 | .replace(QUERY_PREFIX, EMPTY) 22 | .split(AMPERSAND) 23 | .map(couple => { 24 | const split = couple.split(EQUALS); 25 | return { [split[0]]: split[1] }; 26 | }) 27 | .reduce((acc, param) => ({ ...acc, ...param }), {}) : 28 | EMPTY_QUERY; 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/strings.js: -------------------------------------------------------------------------------- 1 | export const upperCaseFirst = (sentence = '') => ( 2 | sentence && typeof sentence === 'string' && sentence.length ? 3 | `${sentence[0].toUpperCase()}${sentence.slice(1, sentence.length)}` : 4 | '' 5 | ); 6 | 7 | export const removeFirst = sentence => sentence.slice(1, sentence.length); 8 | -------------------------------------------------------------------------------- /src/lib/url.js: -------------------------------------------------------------------------------- 1 | export const getHostURL = () => `${document.location.protocol}//${document.location.host}`; -------------------------------------------------------------------------------- /src/lib/utils/assets.js: -------------------------------------------------------------------------------- 1 | import { DIVIDER } from '../constants'; 2 | 3 | export const buildAssetId = (name, level) => level ? `${level}_${name}` : name; 4 | export const isLevelName = level => level.startsWith(DIVIDER); -------------------------------------------------------------------------------- /src/lib/uuid.js: -------------------------------------------------------------------------------- 1 | export const generateUUID = () => { 2 | const s4 = () => Math 3 | .floor((1 + Math.random()) * 0x10000) 4 | .toString(16) 5 | .substring(1); 6 | 7 | return s4() + s4(); 8 | }; 9 | 10 | export const generateRandomName = (prefix) => `${prefix}_${generateUUID()}`; -------------------------------------------------------------------------------- /src/lib/workers.js: -------------------------------------------------------------------------------- 1 | import { WORKERS_NOT_AVAILABLE } from './messages'; 2 | import Features, { FEATURES } from './features'; 3 | 4 | const BLOB_TYPE = 'application/javascript'; 5 | 6 | export const createBlob = (task) => ( 7 | new Blob(['(', task.toString(), ')()'], { type: BLOB_TYPE }) 8 | ); 9 | 10 | export const createWorker = (task, message) => { 11 | if (Features.isFeatureSupported(FEATURES.WEBWORKER)) { 12 | const url = URL.createObjectURL(createBlob(task)); 13 | const worker = new Worker(url); 14 | 15 | // Won't be needing this anymore 16 | URL.revokeObjectURL(url); 17 | 18 | if (message) { 19 | worker.postMessage(message); 20 | } 21 | 22 | return worker; 23 | } else { 24 | console.error(WORKERS_NOT_AVAILABLE); 25 | return null; 26 | } 27 | } 28 | 29 | export const createPromiseWorker = (task, message) => { 30 | return new Promise((resolve, reject) => { 31 | const worker = createWorker(task, message); 32 | 33 | if (worker) { 34 | worker.onmessage = (data) => { 35 | resolve(data); 36 | worker.terminate(); 37 | }; 38 | } else { 39 | reject(WORKERS_NOT_AVAILABLE); 40 | } 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /src/lights/HemisphereLight.js: -------------------------------------------------------------------------------- 1 | import Light from "./light"; 2 | import Scene from "../core/Scene"; 3 | import { HemisphereLight as THREEHemisphereLight, HemisphereLightHelper } from "three"; 4 | import { ENTITY_TYPES } from "../entities/constants"; 5 | import { generateRandomName } from "../lib/uuid"; 6 | import { serializeColor } from "../lib/meshUtils"; 7 | 8 | const DEFAULT_INTENSITY = 0.5; 9 | 10 | const DEFAULT_SKY_COLOR = 0xffffff; 11 | const DEFAULT_GROUND_COLOR = 0x555555; 12 | 13 | const GREEN = 0x2ecc71; 14 | 15 | export default class HemisphereLight extends Light { 16 | constructor(options = {}) { 17 | const { 18 | color = { 19 | sky: DEFAULT_SKY_COLOR, 20 | ground: DEFAULT_GROUND_COLOR, 21 | }, 22 | intensity = DEFAULT_INTENSITY, 23 | name = generateRandomName("HemisphereLight"), 24 | } = options; 25 | 26 | super({ color, intensity, name }); 27 | this.options = options; 28 | this.setLight({ color, intensity }); 29 | this.setEntityType(ENTITY_TYPES.LIGHT.HEMISPHERE); 30 | this.setName(name); 31 | } 32 | 33 | setLight({ 34 | light, 35 | color = { 36 | sky: DEFAULT_SKY_COLOR, 37 | ground: DEFAULT_GROUND_COLOR, 38 | }, 39 | intensity = DEFAULT_INTENSITY, 40 | }) { 41 | if (light) { 42 | this.setBody({ body: light }); 43 | } else { 44 | const { sky = DEFAULT_SKY_COLOR, ground = DEFAULT_GROUND_COLOR } = color; 45 | this.setBody({ body: new THREEHemisphereLight(sky, ground, intensity) }); 46 | } 47 | 48 | if (this.hasBody()) { 49 | this.addToScene(); 50 | } 51 | } 52 | 53 | setColor = ({ sky, ground } = {}) => { 54 | this.getBody().color = sky; 55 | this.getBody().groundColor = ground; 56 | }; 57 | 58 | getColor = () => { 59 | return { 60 | sky: this.getBody().color, 61 | ground: this.getBody().groundColor, 62 | }; 63 | }; 64 | 65 | addHelpers({ holderName = "hemispherelightholder", holderSize = 0.05 } = {}) { 66 | this.helper = new HemisphereLightHelper(this.getBody(), 2, GREEN); 67 | this.addHolder(holderName, holderSize); 68 | 69 | this.isUsingHelper = true; 70 | 71 | Scene.add(this.helper, null, false); 72 | } 73 | 74 | update(dt) { 75 | super.update(dt); 76 | if (this.usingHelper()) { 77 | this.setPosition(this.holder.getPosition(), { updateHolder: false }); 78 | } 79 | } 80 | 81 | toJSON(parseJSON = false) { 82 | const color = this.getColor(); 83 | return { 84 | ...super.toJSON(parseJSON), 85 | color: parseJSON ? serializeColor(color) : color, 86 | }; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/lights/Lights.js: -------------------------------------------------------------------------------- 1 | import { CascadeShadowMaps } from "./csm/CascadeShadowMaps"; 2 | 3 | export const POINTLIGHT = "pointlight"; 4 | export const AMBIENTLIGHT = "ambientlight"; 5 | export const SUNLIGHT = "sunlight"; 6 | export const SPOTLIGHT = "spotlight"; 7 | export const HEMISPHERELIGHT = "hemisphere"; 8 | 9 | export class Lights { 10 | constructor() { 11 | this.lights = []; 12 | this.csm = undefined; 13 | } 14 | 15 | getLights() { 16 | return this.lights; 17 | } 18 | 19 | isUsingCascadeShadowMaps() { 20 | return !!this.csm; 21 | } 22 | 23 | createCascadeShadowMaps(options = {}) { 24 | this.csm = new CascadeShadowMaps(options); 25 | 26 | return this.csm; 27 | } 28 | 29 | add(light) { 30 | this.lights.push(light); 31 | } 32 | 33 | update(dt) { 34 | if (this.isUsingCascadeShadowMaps()) { 35 | this.csm.update(); 36 | } 37 | } 38 | 39 | toJSON(parseJSON = false) { 40 | return { 41 | lights: this.lights.map(l => l.toJSON(parseJSON)), 42 | }; 43 | } 44 | } 45 | 46 | export default new Lights(); 47 | -------------------------------------------------------------------------------- /src/lights/ambientLight.js: -------------------------------------------------------------------------------- 1 | import Light from "./Light"; 2 | import { AmbientLight as THREEAmbientLight } from "three"; 3 | import { AMBIENTLIGHT } from "./Lights"; 4 | import { ENTITY_TYPES } from "../entities/constants"; 5 | import { generateRandomName } from "../lib/uuid"; 6 | 7 | const DEFAULT_POSITION = { x: 0, y: 0, z: 0 }; 8 | const DEFAULT_INTENSITY = 0.5; 9 | const WHITE = 0xffffff; 10 | 11 | export default class AmbientLight extends Light { 12 | constructor(options = {}) { 13 | const { 14 | color = WHITE, 15 | intensity = DEFAULT_INTENSITY, 16 | name = generateRandomName("AmbientLight"), 17 | } = options; 18 | super({ color, intensity, name }); 19 | 20 | this.options = options; 21 | this.setLight({ color, intensity }); 22 | this.setEntityType(ENTITY_TYPES.LIGHT.AMBIENT); 23 | this.setName(name); 24 | } 25 | 26 | setLight({ light, color = WHITE, intensity = DEFAULT_INTENSITY }) { 27 | if (light) { 28 | this.setBody({ body: light }); 29 | } else { 30 | this.setBody({ body: new THREEAmbientLight(color, intensity) }); 31 | } 32 | 33 | if (this.hasBody()) { 34 | this.postLightCreation(); 35 | } 36 | } 37 | 38 | postLightCreation() { 39 | const { position = DEFAULT_POSITION } = this.options; 40 | 41 | this.setPosition(position); 42 | this.addToScene(); 43 | } 44 | 45 | addHelpers({ holderName = "ambientlightholder", holderSize = 0.05 } = {}) { 46 | this.addHolder(holderName, holderSize); 47 | this.isUsingHelper = true; 48 | } 49 | 50 | update(dt) { 51 | super.update(dt); 52 | if (this.usingHelper()) { 53 | this.setPosition(this.holder.getPosition(), { updateHolder: false }); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/lights/constants.js: -------------------------------------------------------------------------------- 1 | export const SHADOW_TYPES = { 2 | BASIC: 'BASIC', 3 | SOFT: 'SOFT', 4 | HARD: 'HARD', 5 | }; 6 | 7 | export const DEFAULT_SHADOWTYPE = SHADOW_TYPES.BASIC; 8 | -------------------------------------------------------------------------------- /src/lights/utils.js: -------------------------------------------------------------------------------- 1 | import { BasicShadowMap, PCFShadowMap, PCFSoftShadowMap } from "three"; 2 | import { DEFAULT_SHADOWTYPE, SHADOW_TYPES } from "./constants"; 3 | 4 | export const mapShadowTypeToShadowMap = (shadowType = DEFAULT_SHADOWTYPE) => ({ 5 | [SHADOW_TYPES.BASIC]: BasicShadowMap, 6 | [SHADOW_TYPES.SOFT]: PCFSoftShadowMap, 7 | [SHADOW_TYPES.HARD]: PCFShadowMap 8 | })[shadowType] || BasicShadowMap; -------------------------------------------------------------------------------- /src/loaders/Loader.js: -------------------------------------------------------------------------------- 1 | import { 2 | ObjectLoader 3 | } from 'three'; 4 | 5 | export default class Loader { 6 | 7 | constructor() { 8 | this.loader = new ObjectLoader(); 9 | } 10 | 11 | load() {} 12 | } 13 | -------------------------------------------------------------------------------- /src/loaders/OBJMTLLoader.js: -------------------------------------------------------------------------------- 1 | import MTLLoader from "./MTLLoader"; 2 | import OBJLoader from "./OBJLoader"; 3 | import RequirementsTracer, { MODELS_REQUIREMENTS } from "./RequirementsTracer"; 4 | 5 | const EXTENSIONS = { 6 | OBJ: ".obj", 7 | MTL: ".mtl", 8 | }; 9 | 10 | export const buildOBJMTLLoader = () => { 11 | const tracer = new RequirementsTracer(); 12 | 13 | class OBJMTLLoader { 14 | constructor(options) { 15 | this.options = options || {}; 16 | this.materialLoader = new MTLLoader(); 17 | this.objLoader = new OBJLoader(); 18 | } 19 | 20 | setOptions(options) { 21 | this.options = options; 22 | this.materialLoader.setOptions(options); 23 | } 24 | 25 | tryLoadingMTL(path) { 26 | const { material } = this.options; 27 | const materialPath = material || path.replace(EXTENSIONS.OBJ, EXTENSIONS.MTL); 28 | 29 | return new Promise((resolve, reject) => { 30 | this.materialLoader.load( 31 | materialPath, 32 | material => { 33 | material.preload(); 34 | resolve(material); 35 | }, 36 | () => {}, 37 | () => { 38 | tracer.trace(MODELS_REQUIREMENTS.MATERIAL); 39 | reject(); 40 | }, 41 | ); 42 | }); 43 | } 44 | 45 | loadObj(path, material) { 46 | if (material) { 47 | this.objLoader.setMaterials(material); 48 | } 49 | 50 | return new Promise(resolve => this.objLoader.load(path, resolve)); 51 | } 52 | 53 | load(path, onComplete) { 54 | this.tryLoadingMTL(path) 55 | .then(material => this.loadObj(path, material)) 56 | .then(onComplete); 57 | } 58 | } 59 | 60 | return { tracer, loader: new OBJMTLLoader() }; 61 | }; 62 | -------------------------------------------------------------------------------- /src/loaders/RequirementsTracer.js: -------------------------------------------------------------------------------- 1 | import { EventDispatcher } from "three"; 2 | 3 | export const REQUIREMENTS_EVENTS = { 4 | MISSING: "requirements:missing", 5 | FULFILLED: "requirements:fulfilled", 6 | }; 7 | 8 | export const MODELS_REQUIREMENTS = { 9 | TEXTURE: "texture", 10 | MATERIAL: "material", 11 | BINARY: "binary", 12 | }; 13 | 14 | export default class RequirementsTracer extends EventDispatcher { 15 | constructor() { 16 | super(); 17 | this.requirements = []; 18 | } 19 | 20 | trace(requirement) { 21 | this.requirements.push(requirement); 22 | this.dispatchEvent({ type: REQUIREMENTS_EVENTS.MISSING, requirements: this.requirements }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/loaders/curves/NURBSCurve.js: -------------------------------------------------------------------------------- 1 | import { 2 | Curve, 3 | Vector3, 4 | Vector4 5 | } from 'three'; 6 | import * as NURBSUtils from '../curves/NURBSUtils.js'; 7 | 8 | /** 9 | * NURBS curve object 10 | * 11 | * Derives from Curve, overriding getPoint and getTangent. 12 | * 13 | * Implementation is based on (x, y [, z=0 [, w=1]]) control points with w=weight. 14 | * 15 | **/ 16 | 17 | class NURBSCurve extends Curve { 18 | 19 | constructor( 20 | degree, 21 | knots /* array of reals */, 22 | controlPoints /* array of Vector(2|3|4) */, 23 | startKnot /* index in knots */, 24 | endKnot /* index in knots */ 25 | ) { 26 | 27 | super(); 28 | 29 | this.degree = degree; 30 | this.knots = knots; 31 | this.controlPoints = []; 32 | // Used by periodic NURBS to remove hidden spans 33 | this.startKnot = startKnot || 0; 34 | this.endKnot = endKnot || ( this.knots.length - 1 ); 35 | 36 | for ( let i = 0; i < controlPoints.length; ++ i ) { 37 | 38 | // ensure Vector4 for control points 39 | const point = controlPoints[ i ]; 40 | this.controlPoints[ i ] = new Vector4( point.x, point.y, point.z, point.w ); 41 | 42 | } 43 | 44 | } 45 | 46 | getPoint( t, optionalTarget = new Vector3() ) { 47 | 48 | const point = optionalTarget; 49 | 50 | const u = this.knots[ this.startKnot ] + t * ( this.knots[ this.endKnot ] - this.knots[ this.startKnot ] ); // linear mapping t->u 51 | 52 | // following results in (wx, wy, wz, w) homogeneous point 53 | const hpoint = NURBSUtils.calcBSplinePoint( this.degree, this.knots, this.controlPoints, u ); 54 | 55 | if ( hpoint.w !== 1.0 ) { 56 | 57 | // project to 3D space: (wx, wy, wz, w) -> (x, y, z, 1) 58 | hpoint.divideScalar( hpoint.w ); 59 | 60 | } 61 | 62 | return point.set( hpoint.x, hpoint.y, hpoint.z ); 63 | 64 | } 65 | 66 | getTangent( t, optionalTarget = new Vector3() ) { 67 | 68 | const tangent = optionalTarget; 69 | 70 | const u = this.knots[ 0 ] + t * ( this.knots[ this.knots.length - 1 ] - this.knots[ 0 ] ); 71 | const ders = NURBSUtils.calcNURBSDerivatives( this.degree, this.knots, this.controlPoints, u, 1 ); 72 | tangent.copy( ders[ 1 ] ).normalize(); 73 | 74 | return tangent; 75 | 76 | } 77 | 78 | } 79 | 80 | export { NURBSCurve }; -------------------------------------------------------------------------------- /src/loaders/utils.js: -------------------------------------------------------------------------------- 1 | export const isAbsoluteURL = url => { 2 | return /^(?:\w+:)\/\//.test(url); 3 | }; 4 | -------------------------------------------------------------------------------- /src/materials/constants.js: -------------------------------------------------------------------------------- 1 | import { MATERIALS, TEXTURES } from "../lib/constants"; 2 | 3 | export const MATERIAL_TEXTURE_MAP = { 4 | [MATERIALS.BASIC]: [ 5 | TEXTURES.ALPHA, 6 | TEXTURES.AO, 7 | TEXTURES.ENV, 8 | TEXTURES.LIGHT, 9 | TEXTURES.MAP, 10 | TEXTURES.SPECULAR, 11 | ], 12 | [MATERIALS.LAMBERT]: [ 13 | TEXTURES.ALPHA, 14 | TEXTURES.AO, 15 | TEXTURES.ENV, 16 | TEXTURES.LIGHT, 17 | TEXTURES.MAP, 18 | TEXTURES.SPECULAR, 19 | TEXTURES.EMISSIVE, 20 | ], 21 | [MATERIALS.PHONG]: [ 22 | TEXTURES.ALPHA, 23 | TEXTURES.AO, 24 | TEXTURES.ENV, 25 | TEXTURES.LIGHT, 26 | TEXTURES.MAP, 27 | TEXTURES.SPECULAR, 28 | TEXTURES.EMISSIVE, 29 | TEXTURES.BUMP, 30 | TEXTURES.DISPLACEMENT, 31 | TEXTURES.NORMAL, 32 | ], 33 | [MATERIALS.DEPTH]: [TEXTURES.ALPHA, TEXTURES.MAP, TEXTURES.DISPLACEMENT], 34 | [MATERIALS.STANDARD]: [ 35 | TEXTURES.ALPHA, 36 | TEXTURES.AO, 37 | TEXTURES.ENV, 38 | TEXTURES.LIGHT, 39 | TEXTURES.MAP, 40 | TEXTURES.EMISSIVE, 41 | TEXTURES.BUMP, 42 | TEXTURES.DISPLACEMENT, 43 | TEXTURES.NORMAL, 44 | TEXTURES.METALNESS, 45 | TEXTURES.ROUGHNESS, 46 | ], 47 | [MATERIALS.THREE_TOON]: [ 48 | TEXTURES.ALPHA, 49 | TEXTURES.AO, 50 | TEXTURES.BUMP, 51 | TEXTURES.DISPLACEMENT, 52 | TEXTURES.EMISSIVE, 53 | TEXTURES.GRADIENT, 54 | TEXTURES.LIGHT, 55 | TEXTURES.MAP, 56 | TEXTURES.NORMAL, 57 | ], 58 | }; 59 | -------------------------------------------------------------------------------- /src/materials/helpers.js: -------------------------------------------------------------------------------- 1 | import { MATERIAL_TEXTURE_MAP } from "./constants"; 2 | 3 | export const isTextureMapAllowedForMaterial = (materialType, textureType) => 4 | MATERIAL_TEXTURE_MAP[materialType] && MATERIAL_TEXTURE_MAP[materialType].includes(textureType); 5 | -------------------------------------------------------------------------------- /src/physics/constants.js: -------------------------------------------------------------------------------- 1 | export const LIBRARY_NAME = 'ammo.js'; 2 | 3 | export const TYPES = { 4 | BOX: 'BOX', 5 | SPHERE: 'SPHERE', 6 | VEHICLE: 'VEHICLE', 7 | MESH: 'MESH', 8 | PLAYER: 'PLAYER' 9 | }; 10 | 11 | export const COLLIDER_TYPES = { 12 | BOX: 'BOX', 13 | VEHICLE: 'VEHICLE', 14 | PLAYER: 'PLAYER', 15 | SPHERE: 'SPHERE' 16 | }; 17 | 18 | export const DEFAULT_VEHICLE_STATE = { 19 | vehicleSteering: 0, 20 | acceleration: false, 21 | breaking: false, 22 | right: false, 23 | left: false 24 | }; 25 | 26 | export const DEFAULT_RIGIDBODY_STATE = { 27 | velocity: { x: 0, y: 0, z: 0 }, 28 | movement: { 29 | forward: false, 30 | backwards: false, 31 | left: false, 32 | right: false 33 | }, 34 | direction: { 35 | x: 0, 36 | y: 0, 37 | z: 0 38 | } 39 | }; 40 | 41 | export const DEFAULT_SCALE = { x: 1, y: 1, z: 1 }; 42 | export const DEFAULT_QUATERNION = { x: 0, y: 0, z: 0, w: 1 }; 43 | export const DEFAULT_POSITION = { x: 0, y: 0, z: 0 }; 44 | export const DEFAULT_LINEAR_VELOCITY = { x: 0, y: 0, z: 0 }; 45 | export const DEFAULT_ANGULAR_VELOCITY = { x: 0, y: 0, z: 0 }; 46 | export const DEFAULT_IMPULSE = { x: 0, y: 0, z: 0 }; 47 | 48 | export const DISABLE_DEACTIVATION = 4; 49 | export const GRAVITY = { x: 0, y: -10, z: 0 }; 50 | 51 | export const FRONT_LEFT = 0; 52 | export const FRONT_RIGHT = 1; 53 | export const BACK_LEFT = 2; 54 | export const BACK_RIGHT = 3; 55 | 56 | export const DEFAULT_STEERING_INCREMENT = .04; 57 | export const DEFAULT_STEERING_CLAMP = .5; 58 | export const DEFAULT_MAX_ENGINE_FORCE = 2000; 59 | export const DEFAULT_MAX_BREAKING_FORCE = 100; 60 | 61 | export const EXPLOSION_SIZES = { 62 | SMALL: 4, 63 | MEDIUM: 6, 64 | LARGE: 8, 65 | MASSIVE: 12 66 | }; 67 | 68 | export const EXPLOSION_STRENGTHS = { 69 | VERY_WEAK: 2, 70 | WEAK: 4, 71 | MEDIUM: 8, 72 | LARGE: 16, 73 | MASSIVE: 32, 74 | OK_NO: 64 75 | }; -------------------------------------------------------------------------------- /src/physics/hitbox.js: -------------------------------------------------------------------------------- 1 | import { Vector3 } from 'three'; 2 | import Box from '../entities/base/Box'; 3 | import Sphere from '../entities/base/Sphere'; 4 | import { COLLIDER_TYPES } from './constants'; 5 | 6 | const HIT_BOX_COLOR = 0xf368e0; 7 | const HIT_BOX_INCREASE = .03; 8 | 9 | const DEFAULT_HITBOX_OPTIONS = { 10 | shadowsEnabled: false 11 | }; 12 | 13 | export const getBoxHitbox = element => { 14 | const size = new Vector3(); 15 | element.boundingBox.getSize(size); 16 | 17 | const scaledSize = { 18 | x: size.x + HIT_BOX_INCREASE, 19 | y: size.y + HIT_BOX_INCREASE, 20 | z: size.z + HIT_BOX_INCREASE 21 | }; 22 | const box = new Box(scaledSize.x, scaledSize.y, scaledSize.z, HIT_BOX_COLOR, DEFAULT_HITBOX_OPTIONS); 23 | 24 | //box.setQuaternion(quaternion); 25 | box.setWireframe(true); 26 | box.setWireframeLineWidth(2); 27 | 28 | return box; 29 | } 30 | 31 | export const getSphereHitbox = element => { 32 | const radius = element.boundingSphere.radius; 33 | const sphere = new Sphere(radius, HIT_BOX_COLOR, DEFAULT_HITBOX_OPTIONS); 34 | 35 | sphere.setWireframe(true); 36 | sphere.setWireframeLineWidth(2); 37 | 38 | return sphere; 39 | }; 40 | 41 | export const mapColliderTypeToHitbox = (colliderType = COLLIDER_TYPES.BOX) => ({ 42 | [COLLIDER_TYPES.BOX]: getBoxHitbox, 43 | [COLLIDER_TYPES.SPHERE]: getSphereHitbox 44 | }[colliderType] || getBoxHitbox); 45 | 46 | export const addHitBox = element => { 47 | const colliderType = element.getPhysicsOptions('colliderType'); 48 | const getHitbox = mapColliderTypeToHitbox(colliderType) 49 | 50 | element.add(getHitbox(element)); 51 | }; -------------------------------------------------------------------------------- /src/physics/messages.js: -------------------------------------------------------------------------------- 1 | export const PHYSICS_EVENTS = { 2 | DISPATCH: 'physics:dispatch', 3 | TERMINATE: 'physics:terminate', 4 | LOAD: { 5 | AMMO: 'physics:load:ammo', 6 | }, 7 | READY: 'physics:ready', 8 | INIT: 'physics:init', 9 | UPDATE: 'physics:update', 10 | 11 | ADD: { 12 | BOX: 'physics:add:box', 13 | VEHICLE: 'physics:add:vehicle', 14 | MODEL: 'physics:add:model', 15 | PLAYER: 'physics:add:player', 16 | SPHERE: 'physics:add:sphere', 17 | }, 18 | 19 | ELEMENT: { 20 | DISPOSE: 'physics:element:dispose', 21 | COLLISION: 'physics:element:collision', 22 | UPDATE: 'physics:element:update', 23 | CREATED: 'physics:element:created', 24 | 25 | SET: { 26 | POSITION: 'physics:element:set:position', 27 | QUATERNION: 'physics:element:set:quaternion', 28 | LINEAR_VELOCITY: 'physics:element:set:linear_velocity' 29 | }, 30 | 31 | RESET: 'physics:element:reset', 32 | 33 | APPLY: { 34 | IMPULSE: 'physics:element:apply:impulse' 35 | } 36 | }, 37 | 38 | VEHICLE: { 39 | SET: { 40 | POSITION: 'physics:vehicle:set:position', 41 | QUATERNION: 'physics:vehicle:set:quaternion' 42 | }, 43 | RESET: 'physics:vehicle:reset', 44 | 45 | SPEED: 'physics:vehicle:speed', 46 | DIRECTION: 'physics:vehicle:direction' 47 | }, 48 | 49 | EFFECTS: { 50 | EXPLOSION: 'physics:effects:explosion' 51 | } 52 | }; -------------------------------------------------------------------------------- /src/physics/worker/effects.js: -------------------------------------------------------------------------------- 1 | import { EXPLOSION_SIZES, EXPLOSION_STRENGTHS } from "../constants"; 2 | import world from "./world"; 3 | 4 | export const createGhostCollider = (radius, position) => { 5 | const ghostCollider = new Ammo.btGhostObject(); 6 | const transform = new Ammo.btTransform(); 7 | 8 | ghostCollider.setCollisionShape(new Ammo.btSphereShape(radius)); 9 | ghostCollider.getWorldTransform(transform); 10 | 11 | transform.setIdentity(); 12 | transform.setOrigin(position); 13 | transform.setRotation(new Ammo.btQuaternion(0, 0, 0, 1)); 14 | 15 | ghostCollider.setWorldTransform(transform); 16 | 17 | return { ghostCollider, transform }; 18 | }; 19 | 20 | export const forEachGhostCollision = (ghostCollider, forEachCallback = () => {}) => { 21 | const collisions = ghostCollider.getNumOverlappingObjects(); 22 | for (let i=0; i { 34 | let explosionPosition = position; 35 | if (!explosionPosition) { 36 | const { body } = world.getElement(uuid); 37 | const motionState = body.getMotionState(); 38 | const transform = new Ammo.btTransform(); 39 | 40 | motionState.getWorldTransform(transform); 41 | explosionPosition = transform.getOrigin(); 42 | } 43 | 44 | return explosionPosition; 45 | }; 46 | 47 | export const getExplosionImpulse = (position, explosionPosition, strength) => { 48 | const distance = position.op_sub(explosionPosition); 49 | distance.normalize(); 50 | const impulse = distance.op_mul(strength); 51 | 52 | impulse.setY(impulse.y() + strength); 53 | 54 | return impulse; 55 | } 56 | 57 | export const createExplosion = ({ 58 | uuid, 59 | position, 60 | radius = EXPLOSION_SIZES.SMALL, 61 | strength = EXPLOSION_STRENGTHS.MEDIUM 62 | }) => { 63 | try { 64 | const explosionPosition = getExplosionPosition(uuid, position); 65 | const { ghostCollider, transform } = createGhostCollider(radius, explosionPosition); 66 | 67 | world.addCollisionObject(ghostCollider); 68 | 69 | forEachGhostCollision(ghostCollider, (object, objectTransform) => { 70 | const origin = objectTransform.getOrigin(); 71 | object.activate(true); 72 | object.applyCentralImpulse(getExplosionImpulse(origin, explosionPosition, strength)); 73 | }); 74 | 75 | world.getDynamicsWorld().removeCollisionObject(ghostCollider); 76 | Ammo.destroy(ghostCollider); 77 | Ammo.destroy(transform); 78 | } catch(e) { 79 | console.log(e); 80 | } 81 | } -------------------------------------------------------------------------------- /src/physics/worker/lib/dispatcher.js: -------------------------------------------------------------------------------- 1 | import { PHYSICS_EVENTS } from '../../messages'; 2 | 3 | export class Dispatcher { 4 | 5 | sendPhysicsUpdate = dt => postMessage({ event: PHYSICS_EVENTS.UPDATE, dt }) 6 | sendReadyEvent = () => postMessage({ event: PHYSICS_EVENTS.READY }); 7 | sendTerminateEvent = () => postMessage({ event: PHYSICS_EVENTS.TERMINATE }); 8 | 9 | sendBodyUpdate = (uuid, position, rotation, dt, extraData) => postMessage({ 10 | event: PHYSICS_EVENTS.ELEMENT.UPDATE, 11 | uuid, 12 | position: { x: position.x(), y: position.y(), z: position.z() }, 13 | quaternion: { x: rotation.x(), y: rotation.y(), z: rotation.z(), w: rotation.w() }, 14 | ...extraData, 15 | dt 16 | }); 17 | 18 | sendDispatchEvent = (uuid, eventName, eventData) => postMessage({ 19 | event: PHYSICS_EVENTS.DISPATCH, 20 | uuid, 21 | eventName, 22 | eventData 23 | }); 24 | }; 25 | 26 | export default new Dispatcher(); -------------------------------------------------------------------------------- /src/physics/worker/lib/math.js: -------------------------------------------------------------------------------- 1 | export const applyMatrix4ToVector3 = ({ x = 0, y = 0, z = 0 }, matrix = []) => { 2 | const w = 1 / ( matrix[ 3 ] * x + matrix[ 7 ] * y + matrix[ 11 ] * z + matrix[ 15 ] ); 3 | 4 | return { 5 | x: ( matrix[ 0 ] * x + matrix[ 4 ] * y + matrix[ 8 ] * z + matrix[ 12 ] ) * w, 6 | y: ( matrix[ 1 ] * x + matrix[ 5 ] * y + matrix[ 9 ] * z + matrix[ 13 ] ) * w, 7 | z: ( matrix[ 2 ] * x + matrix[ 6 ] * y + matrix[ 10 ] * z + matrix[ 14 ] ) * w, 8 | } 9 | }; -------------------------------------------------------------------------------- /src/physics/worker/player.js: -------------------------------------------------------------------------------- 1 | import { createRigidBody } from './elements'; 2 | import world from './world'; 3 | 4 | import { 5 | TYPES, 6 | DEFAULT_RIGIDBODY_STATE 7 | } from '../constants'; 8 | 9 | export const addPlayer = (data) => { 10 | const { uuid, width, height, position, quaternion, mass, friction } = data; 11 | 12 | const capsule = new Ammo.btCapsuleShape(width, height); 13 | const body = createRigidBody(capsule, { uuid, position, quaternion, mass, friction }); 14 | 15 | // disabliing rotation for collisions 16 | body.setAngularFactor(0); 17 | 18 | world.addRigidBody(body); 19 | world.addElement({ uuid, body, type: TYPES.PLAYER, state: DEFAULT_RIGIDBODY_STATE }); 20 | }; 21 | 22 | export const handlePlayerUpdate = ({ body, uuid, state = DEFAULT_RIGIDBODY_STATE }, dt) => { 23 | const { movement, direction, cameraDirection, quaternion, position } = state; 24 | 25 | const MAX_SPEED = 1; 26 | const walkVelocity = .1; 27 | 28 | const motionState = body.getMotionState(); 29 | 30 | if (motionState) { 31 | const transform = new Ammo.btTransform(); 32 | motionState.getWorldTransform(transform); 33 | 34 | const linearVelocity = body.getLinearVelocity(); 35 | const speed = linearVelocity.length(); 36 | 37 | const forwardDir = transform.getBasis().getRow(2); 38 | forwardDir.normalize(); 39 | const walkDirection = new Ammo.btVector3(0.0, 0.0, 0.0); 40 | 41 | const walkSpeed = walkVelocity * dt; 42 | 43 | if (movement.forward) { 44 | walkDirection.setX( walkDirection.x() + forwardDir.x()); 45 | //walkDirection.setY( walkDirection.y() + forwardDir.y()); 46 | walkDirection.setZ( walkDirection.z() + forwardDir.z()); 47 | } 48 | 49 | if (movement.backwards) { 50 | walkDirection.setX( walkDirection.x() - forwardDir.x()); 51 | //walkDirection.setY( walkDirection.y() - forwardDir.y()); 52 | walkDirection.setZ( walkDirection.z() - forwardDir.z()); 53 | } 54 | 55 | if (!movement.forward && !movement.backwards) { 56 | linearVelocity.setX(linearVelocity.x() * 0.2); 57 | linearVelocity.setZ(linearVelocity.z() * 0.2); 58 | } else if (speed < MAX_SPEED) { 59 | linearVelocity.setX(linearVelocity.x() + cameraDirection.x * walkSpeed); 60 | linearVelocity.setZ(linearVelocity.z() + cameraDirection.z * walkSpeed); 61 | } 62 | 63 | body.setLinearVelocity(linearVelocity); 64 | 65 | body.getMotionState().setWorldTransform(transform); 66 | body.setCenterOfMassTransform(transform); 67 | 68 | let origin = transform.getOrigin(); 69 | let rotation = transform.getRotation(); 70 | 71 | dispatcher.sendBodyUpdate(uuid, origin, rotation, dt); 72 | } 73 | }; -------------------------------------------------------------------------------- /src/scripts/BaseScript.js: -------------------------------------------------------------------------------- 1 | export default class BaseScript { 2 | __check() { 3 | return true; 4 | } 5 | 6 | __isStatic() { 7 | return false; 8 | } 9 | 10 | __hasStarted() { 11 | return this.hasStarted; 12 | } 13 | 14 | __isDisposed() { 15 | return this.isDisposed; 16 | } 17 | 18 | __setStartedFlag(flag) { 19 | this.hasStarted = flag; 20 | } 21 | 22 | __setDisposedFlag(flag) { 23 | this.isDisposed = flag; 24 | } 25 | 26 | constructor(name) { 27 | this.__name = name || this.constructor.name; 28 | this.hasStarted = false; 29 | this.isDisposed = false; 30 | this.options = {}; 31 | } 32 | 33 | getName() { 34 | return this.__name; 35 | } 36 | 37 | start(element, options) {} 38 | 39 | update(dt) {} 40 | 41 | physicsUpdate(dt) {} 42 | 43 | onDispose() {} 44 | 45 | toJSON(parseJSON = false) { 46 | return { 47 | name: this.name(), 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/scripts/Scripts.js: -------------------------------------------------------------------------------- 1 | import BaseScript from './BaseScript'; 2 | import Input from '../core/input/Input'; 3 | import { fetch } from 'whatwg-fetch'; 4 | import BaseCar from './builtin/BaseCar'; 5 | import SmoothCarFollow from './builtin/SmoothCarFollow'; 6 | import { DEPRECATIONS } from '../lib/messages'; 7 | 8 | export const BUILTIN = { 9 | BASECAR: 'BaseCar', 10 | TRAILS: 'Trails', 11 | SMOOTH_CAR_FOLLOW: 'SmoothCarFollow' 12 | }; 13 | export class Scripts { 14 | 15 | constructor() { 16 | this.map = { 17 | [BUILTIN.BASECAR] : BaseCar, 18 | [BUILTIN.SMOOTH_CAR_FOLLOW]: SmoothCarFollow 19 | }; 20 | } 21 | 22 | get BUILTIN() { 23 | return BUILTIN; 24 | } 25 | 26 | update() {} 27 | 28 | load = (scripts, level) => { 29 | this.scripts = scripts; 30 | const keys = Object.keys(scripts); 31 | 32 | if (!keys.length) { 33 | return Promise.resolve('scripts'); 34 | } 35 | 36 | return Promise.all(keys.map(name => this.loadSingleScript(name, level))); 37 | } 38 | 39 | loadSingleScript = (name, level) => { 40 | const path = this.scripts[name]; 41 | 42 | return new Promise(resolve => { 43 | 44 | fetch(path) 45 | .then(response => response.text()) 46 | .then((text) => { 47 | this.createFromString(text); 48 | resolve(); 49 | }); 50 | }); 51 | } 52 | 53 | set(id, ScriptClass) { 54 | this.map[id] = ScriptClass; 55 | } 56 | 57 | get(name) { 58 | const ScriptClass = this.map[name]; 59 | 60 | if (ScriptClass) { 61 | return new ScriptClass(name); 62 | } 63 | 64 | return false; 65 | } 66 | 67 | parseScript(content) { 68 | // does this mean we can send whatever we want down to the script? 69 | return new Function('Script', 'Input', 'return ' + content + ';')(BaseScript, Input); 70 | } 71 | 72 | createFromString(stringContent) { 73 | const Script = this.parseScript(stringContent); 74 | const s = new Script(); 75 | 76 | this.set(s.name(), s); 77 | return s; 78 | } 79 | 80 | create(name, ScriptClass) { 81 | console.warn(DEPRECATIONS.SCRIPTS_CREATE); 82 | this.register(name, ScriptClass); 83 | } 84 | 85 | register(name, ScriptClass) { 86 | if (ScriptClass) { 87 | const script = new ScriptClass(); 88 | if (script.__check && script.__check()) { 89 | this.set(name, ScriptClass); 90 | } else { 91 | console.error('[Mage] Script:', name, 'needs to be an instance of Script.'); 92 | } 93 | } else { 94 | console.error('[Mage] Script not provided.'); 95 | } 96 | } 97 | } 98 | 99 | export default new Scripts(); 100 | -------------------------------------------------------------------------------- /src/scripts/StaticScript.js: -------------------------------------------------------------------------------- 1 | export default class StaticScript { 2 | __isStatic() { 3 | return true; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/scripts/builtin/BaseCar.js: -------------------------------------------------------------------------------- 1 | import Input from "../../core/input/Input"; 2 | import BaseScript from "../BaseScript"; 3 | import Physics from "../../physics"; 4 | 5 | export default class BaseCar extends BaseScript { 6 | constructor() { 7 | super("BaseCar"); 8 | } 9 | 10 | start(element, options) { 11 | const { 12 | wheels, 13 | accelerationKey = "w", 14 | brakingKey = "s", 15 | rightKey = "d", 16 | leftKey = "a", 17 | debug = false, 18 | autostart = true, 19 | ...physicsOptions 20 | } = options; 21 | 22 | this.car = element; 23 | this.wheels = wheels; 24 | 25 | this.state = { 26 | acceleration: false, 27 | braking: false, 28 | right: false, 29 | left: false, 30 | }; 31 | 32 | this.accelerationKey = accelerationKey; 33 | this.brakingKey = brakingKey; 34 | this.rightKey = rightKey; 35 | this.leftKey = leftKey; 36 | 37 | this.engineStarted = autostart; 38 | 39 | Input.enable(); 40 | Input.keyboard.listenTo([accelerationKey, brakingKey, rightKey, leftKey]); 41 | Physics.addVehicle(this.car, { wheels: wheels.map(w => w.uuid()), ...physicsOptions }); 42 | } 43 | 44 | startEngine() { 45 | this.engineStarted = true; 46 | } 47 | 48 | stopEngine() { 49 | this.engineStarted = false; 50 | } 51 | 52 | handleInput() { 53 | this.state.acceleration = Input.keyboard.isPressed(this.accelerationKey); 54 | this.state.braking = Input.keyboard.isPressed(this.brakingKey); 55 | this.state.right = Input.keyboard.isPressed(this.rightKey); 56 | this.state.left = Input.keyboard.isPressed(this.leftKey); 57 | } 58 | 59 | sendCarUpdate() { 60 | Physics.updateBodyState(this.car, this.state); 61 | } 62 | 63 | update(dt) { 64 | if (this.engineStarted) { 65 | this.handleInput(); 66 | this.sendCarUpdate(); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/scripts/builtin/SmoothCameraFollow.js: -------------------------------------------------------------------------------- 1 | import BaseScript from '../BaseScript'; 2 | 3 | export default class SmoothCameraFollow extends BaseScript { 4 | 5 | constructor() { 6 | super('SmoothCameraFollow'); 7 | } 8 | 9 | start(camera, options) { 10 | const { 11 | target, 12 | distance = 20.0, 13 | height = 5.0, 14 | lookAtHeight = 1.0, 15 | heightDamping = 2.0, 16 | rotationSnapTime = 0.3, 17 | distanceSnapTime = 0.1 18 | } = options; 19 | 20 | this.height = height; 21 | this.distance = distance; 22 | this.heightDamping = heightDamping; 23 | this.rotationSnapTime = rotationSnapTime; 24 | this.distanceSnapTime = distanceSnapTime; 25 | 26 | this.camera = camera; 27 | this.target = target; 28 | 29 | this.yVelocity = 0.0; 30 | this.zVelocity = 0.0; 31 | this.usedDistance = 0.0; 32 | 33 | this.lookAtVector = { x: 0, y: lookAtHeight, z: 0 }; 34 | } 35 | 36 | physicsUpdate(dt) { 37 | 38 | const wantedHeight = this.target.getPosition().y + this.height; 39 | let currentHeight = this.camera.getPosition().y; 40 | 41 | const wantedRotationAngle = this.target.getRotation().y; 42 | const currentRotationAngle = this.camera.getRotation().y; 43 | 44 | const currentRotationAngle = Mathf.SmoothDampAngle(currentRotationAngle, wantedRotationAngle, this.yVelocity, this.rotationSnapTime); 45 | 46 | currentHeight = Mathf.Lerp(currentHeight, wantedHeight, this.heightDamping * dt); 47 | 48 | const wantedPosition = this.target.getPosition(); 49 | wantedPosition.y = currentHeight; 50 | 51 | 52 | this.usedDistance = Mathf.SmoothDampAngle(this.usedDistance, this.distance, this.zVelocity, this.distanceSnapTime); 53 | 54 | wantedPosition += Quaternion.Euler(0, currentRotationAngle, 0) * new Vector3(0, 0, -usedDistance); 55 | 56 | this.camera.setPosition(wantedPosition); 57 | this.camera.lookAt(target.position + lookAtVector); 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /src/scripts/builtin/SmoothCarFollow.js: -------------------------------------------------------------------------------- 1 | import BaseScript from '../BaseScript'; 2 | import { Vector3 } from 'three'; 3 | 4 | const DEFAULT_DISTANCE = 5.0; 5 | const DEFAULT_HEIGHT = 3.0; 6 | const DEFAULT_HEIGHT_DAMPING = 2.0; 7 | const DEFAULT_LOOK_AT_HEIGHT = 1; 8 | const DEFAULT_ROTATION_SNAP_TIME = 0.3; 9 | const DEFAULT_DISTANCE_SNAP_TIME = 0.5; 10 | const DEFAULT_DISTANCE_MULTIPLIER = 1; 11 | 12 | export default class SmoothCarFollow extends BaseScript { 13 | 14 | constructor() { 15 | super('SmoothCarFollow'); 16 | } 17 | 18 | start(camera, options) { 19 | const { 20 | target, 21 | height = DEFAULT_HEIGHT, 22 | heightDamping = DEFAULT_HEIGHT_DAMPING, 23 | lookAtHeight = DEFAULT_LOOK_AT_HEIGHT, 24 | distance = DEFAULT_DISTANCE, 25 | rotationSnapTime = DEFAULT_ROTATION_SNAP_TIME, 26 | distanceSnapTime = DEFAULT_DISTANCE_SNAP_TIME, 27 | distanceMultiplier = DEFAULT_DISTANCE_MULTIPLIER, 28 | lerpFactor 29 | } = options; 30 | 31 | this.camera = camera; 32 | this.target = target; 33 | 34 | this.height = height; 35 | this.heightDamping = heightDamping; 36 | 37 | this.distance = distance; 38 | 39 | this.rotationSnapTime = rotationSnapTime; 40 | this.distanceSnapTime = distanceSnapTime; 41 | 42 | this.distanceMultiplier = distanceMultiplier; 43 | this.lerpFactor = lerpFactor; 44 | 45 | this.lookAtVector = new Vector3(0, lookAtHeight, 0); 46 | } 47 | 48 | followCar(dt) { 49 | const direction = this.target.getPhysicsState('direction'); 50 | if (direction) { 51 | const { x, y, z } = direction; 52 | const cameraPosition = this.camera.getPosition(); 53 | const targetPosition = this.target.getPosition(); 54 | const vector = new Vector3(x, y, z) 55 | .negate() 56 | .normalize() 57 | .multiplyScalar(this.distance); 58 | 59 | vector.y = y + this.height; 60 | const desiredPosition = targetPosition.add(vector); 61 | const lerpFactor = this.lerpFactor || 1 - Math.pow(0.1, dt); 62 | 63 | cameraPosition.lerpVectors(cameraPosition, desiredPosition, lerpFactor); 64 | 65 | this.camera.setPosition(cameraPosition); 66 | 67 | const lookAtTarget = new Vector3(); 68 | lookAtTarget.copy(this.target.getPosition().add(this.lookAtVector)); 69 | 70 | this.camera.lookAt(lookAtTarget); 71 | } 72 | } 73 | 74 | update(dt) { 75 | this.followCar(dt); 76 | } 77 | 78 | } -------------------------------------------------------------------------------- /src/scripts/builtin/Trails.js: -------------------------------------------------------------------------------- 1 | import Particles, { PARTICLES } from "../../fx/particles/Particles"; 2 | 3 | import BaseScript from "../BaseScript" 4 | 5 | export default class Trails extends BaseScript { 6 | 7 | constructor() { 8 | super('Trails'); 9 | } 10 | 11 | start(element, { texture = false, size }) { 12 | this.element = element; 13 | this.emitter = Particles.add(PARTICLES.TRAIL, { texture, size }); 14 | 15 | this.emitter.start(Infinity); 16 | } 17 | 18 | stop() { 19 | this.emitter.dispose(); 20 | } 21 | 22 | update() { 23 | this.emitter.setPosition(this.element.getPosition()); 24 | } 25 | } -------------------------------------------------------------------------------- /src/store/Store.js: -------------------------------------------------------------------------------- 1 | // creates the store 2 | import * as redux from 'redux'; 3 | import thunk from 'redux-thunk'; 4 | 5 | import createRootReducer, * as DEFAULT_REDUCERS from './reducers'; 6 | 7 | import { STORE_DOESNT_EXIST } from '../lib/messages'; 8 | import { NOOP } from '../lib/functions'; 9 | 10 | let store, 11 | latestAction; 12 | 13 | let unsubscribe = NOOP; 14 | let subscribers = []; 15 | 16 | const applyMiddlewares = (mdws, debug) => { 17 | if (debug) { 18 | return redux.compose( 19 | redux.applyMiddleware(...mdws), 20 | window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : f => f 21 | ); 22 | } 23 | return redux.applyMiddleware(...mdws); 24 | } 25 | 26 | const defaultMiddleware = () => [thunk]; 27 | 28 | const combineReducers = (reducers = {}) => ( 29 | redux.combineReducers({ 30 | ...reducers, 31 | ...DEFAULT_REDUCERS 32 | }) 33 | ); 34 | 35 | export const getState = () => { 36 | if (store) { 37 | return store.getState(); 38 | } else { 39 | console.log(STORE_DOESNT_EXIST); 40 | } 41 | }; 42 | 43 | export const getStore = () => store; 44 | 45 | const handleSubscriptions = (...args) => ( 46 | subscribers 47 | .forEach((subscriber) => { 48 | if (subscriber.onStateChange) { 49 | subscriber.onStateChange(getState(), latestAction); 50 | } 51 | }) 52 | ) 53 | 54 | export const createStore = (reducers, initialState = {}, debug = false) => { 55 | const storeReducers = combineReducers(reducers); 56 | 57 | if (!store) { 58 | store = redux.createStore( 59 | createRootReducer(storeReducers), 60 | initialState, 61 | applyMiddlewares(defaultMiddleware(), debug) 62 | ); 63 | unsubscribe = store.subscribe(handleSubscriptions); 64 | } 65 | }; 66 | 67 | export const subscribe = subscriber => subscribers.push(subscriber); 68 | export const unsubscribeAll = () => { 69 | unsubscribe(); 70 | subscribers = []; 71 | } 72 | 73 | export const dispatch = (action) => { 74 | if (store && action) { 75 | store.dispatch(action); 76 | latestAction = action; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/store/actions/input.js: -------------------------------------------------------------------------------- 1 | import { 2 | INPUT_ENABLED, 3 | KEYBOARD_ENABLED, 4 | MOUSE_ENABLED, 5 | GAMEPAD_ENABLED, 6 | INPUT_DISABLED, 7 | KEYBOARD_DISABLED, 8 | MOUSE_DISABLED, 9 | GAMEPAD_DISABLED, 10 | GAMEPAD_CONNECTED, 11 | GAMEPAD_DISCONNECTED 12 | } from "./types"; 13 | 14 | export const inputEnabled = () => ({ 15 | type: INPUT_ENABLED 16 | }); 17 | 18 | export const keyboardEnabled = () => ({ 19 | type: KEYBOARD_ENABLED 20 | }); 21 | 22 | export const mouseEnabled = () => ({ 23 | type: MOUSE_ENABLED 24 | }); 25 | 26 | export const gamepadEnabled = () => ({ 27 | type: GAMEPAD_ENABLED 28 | }); 29 | 30 | export const inputDisabled = () => ({ 31 | type: INPUT_DISABLED 32 | }); 33 | 34 | export const keyboardDisabled = () => ({ 35 | type: KEYBOARD_DISABLED 36 | }); 37 | 38 | export const mouseDisabled = () => ({ 39 | type: MOUSE_DISABLED 40 | }); 41 | 42 | export const gamepadDisabled = () => ({ 43 | type: GAMEPAD_DISABLED 44 | }); 45 | 46 | export const gamepadConnected = (gamepads) => ({ 47 | type: GAMEPAD_CONNECTED, 48 | gamepads 49 | }); 50 | 51 | export const gamepadDisconnected = (gamepads) => ({ 52 | type: GAMEPAD_DISCONNECTED, 53 | gamepads 54 | }); 55 | -------------------------------------------------------------------------------- /src/store/actions/location.js: -------------------------------------------------------------------------------- 1 | import { LOCATION_PATH_CHANGE } from "./types"; 2 | 3 | export const locationPathChange = path => ({ 4 | type: LOCATION_PATH_CHANGE, 5 | path 6 | }); -------------------------------------------------------------------------------- /src/store/actions/network.js: -------------------------------------------------------------------------------- 1 | import { 2 | NETWORK_CHANGED 3 | } from './types'; 4 | 5 | export const networkChanged = isOnline => ({ 6 | type: NETWORK_CHANGED, 7 | isOnline 8 | }); -------------------------------------------------------------------------------- /src/store/actions/storage.js: -------------------------------------------------------------------------------- 1 | import { 2 | SAVE_STARTED, 3 | SAVE_COMPLETED, 4 | SAVE_ERROR, 5 | LOAD_STARTED, 6 | LOAD_COMPLETED, 7 | LOAD_ERROR, 8 | RESET 9 | } from './types'; 10 | 11 | import storage from '../../storage/storage'; 12 | 13 | export const saveStarted = () => ({ 14 | type: SAVE_STARTED 15 | }); 16 | 17 | export const saveCompleted = (sceneName, currentPath, timestamp) => ({ 18 | type: SAVE_COMPLETED, 19 | timestamp, 20 | sceneName, 21 | currentPath 22 | }); 23 | 24 | export const saveError = (errorDetails) => ({ 25 | type: SAVE_ERROR, 26 | errorDetails 27 | }); 28 | 29 | export const loadStarted = () => ({ 30 | type: LOAD_STARTED 31 | }); 32 | 33 | export const loadCompleted = () => ({ 34 | type: LOAD_COMPLETED 35 | }); 36 | 37 | export const loadError = (errorDetails) => ({ 38 | type: LOAD_ERROR, 39 | errorDetails 40 | }); 41 | 42 | export const resetState = (state) => ({ 43 | type: RESET, 44 | state 45 | }); 46 | 47 | export const saveGame = () => (dispatch) => { 48 | dispatch(saveStarted()); 49 | 50 | storage 51 | .save() 52 | .then(({ sceneName, currentPath, timestamp }) => 53 | dispatch(saveCompleted(sceneName, currentPath, timestamp)) 54 | ) 55 | .catch((e) => dispatch(saveError(e))); 56 | }; 57 | 58 | export const loadGame = () => (dispatch) => { 59 | dispatch(loadStarted()); 60 | 61 | storage 62 | .loadState() 63 | .then(state => dispatch(resetState(state))) 64 | .catch((e) => dispatch(loadError(e))); 65 | 66 | storage 67 | .loadCurrentPath() 68 | .then(currentPath => Router.goTo(currentPath, { loading: true })) 69 | .then(() => dispatch(loadCompleted())) 70 | .catch((e) => dispatch(loadError(e))); 71 | } 72 | -------------------------------------------------------------------------------- /src/store/actions/types.js: -------------------------------------------------------------------------------- 1 | export const SAVE_STARTED = 'SAVE_STARTED'; 2 | export const SAVE_COMPLETED = 'SAVE_COMPLETED'; 3 | export const SAVE_ERROR = 'SAVE_ERROR'; 4 | 5 | export const LOAD_STARTED = 'LOAD_STARTED'; 6 | export const LOAD_COMPLETED= 'LOAD_COMPLETED'; 7 | export const LOAD_ERROR = 'LOAD_ERROR'; 8 | 9 | export const KEYBOARD_ENABLED = 'KEYBOARD_ENABLED'; 10 | export const MOUSE_ENABLED = 'MOUSE_ENABLED'; 11 | export const GAMEPAD_ENABLED = 'GAMEPAD_ENABLED'; 12 | export const INPUT_ENABLED = 'INPUT_ENABLED'; 13 | 14 | export const KEYBOARD_DISABLED = 'KEYBOARD_DISABLED'; 15 | export const MOUSE_DISABLED = 'MOUSE_DISABLED'; 16 | export const GAMEPAD_DISABLED = 'GAMEPAD_DISABLED'; 17 | export const INPUT_DISABLED = 'INPUT_DISABLED'; 18 | 19 | export const GAMEPAD_CONNECTED = 'GAMEPAD_CONNECTED'; 20 | export const GAMEPAD_DISCONNECTED = 'GAMEPAD_DISCONNECTED'; 21 | 22 | export const NETWORK_CHANGED = 'NETWORK_CHANGED'; 23 | 24 | export const UI_LOADING_SCREEN = 'UI_LOADING_SCREEN'; 25 | 26 | export const LOCATION_PATH_CHANGE = 'LOCATION_PATH_CHANGE'; 27 | 28 | export const RESET = 'RESET'; 29 | -------------------------------------------------------------------------------- /src/store/actions/ui.js: -------------------------------------------------------------------------------- 1 | import { 2 | UI_LOADING_SCREEN 3 | } from './types'; 4 | 5 | export const showLoadingScreen = () => ({ 6 | type: UI_LOADING_SCREEN, 7 | loadingScreenVisible: true 8 | }); 9 | 10 | export const hideLoadingScreen = () => ({ 11 | type: UI_LOADING_SCREEN, 12 | loadingScreenVisible: false 13 | }); -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | getState, 3 | createStore, 4 | dispatch 5 | } from './Store'; 6 | 7 | export { 8 | getState, 9 | createStore, 10 | dispatch 11 | }; 12 | -------------------------------------------------------------------------------- /src/store/reducers/index.js: -------------------------------------------------------------------------------- 1 | import info from './info'; 2 | import storage from './storage'; 3 | import input from './input'; 4 | import network from './network'; 5 | import ui from './ui'; 6 | import location from './location'; 7 | import { createRootReducer } from './root'; 8 | 9 | export { 10 | info, 11 | input, 12 | storage, 13 | network, 14 | ui, 15 | location, 16 | }; 17 | 18 | export default createRootReducer; 19 | -------------------------------------------------------------------------------- /src/store/reducers/info.js: -------------------------------------------------------------------------------- 1 | import packageJSON from '../../../package.json'; 2 | 3 | const DEFAULT_STATE = { 4 | mage: packageJSON.version 5 | } 6 | 7 | export default (state = DEFAULT_STATE, action = {}) => { 8 | return state; 9 | }; 10 | -------------------------------------------------------------------------------- /src/store/reducers/input.js: -------------------------------------------------------------------------------- 1 | import { 2 | INPUT_ENABLED, 3 | KEYBOARD_ENABLED, 4 | MOUSE_ENABLED, 5 | GAMEPAD_ENABLED, 6 | INPUT_DISABLED, 7 | KEYBOARD_DISABLED, 8 | MOUSE_DISABLED, 9 | GAMEPAD_DISABLED, 10 | GAMEPAD_CONNECTED, 11 | GAMEPAD_DISCONNECTED 12 | } from "../actions/types"; 13 | 14 | const DEFAULT_STATE = { 15 | keyboard: false, 16 | mouse: false, 17 | gamepad: false, 18 | gamepads: {} 19 | }; 20 | 21 | export default (state = DEFAULT_STATE, action = {}) => { 22 | switch(action.type) { 23 | case INPUT_ENABLED: 24 | return { 25 | ...state, 26 | keyboard: true, 27 | mouse: true, 28 | gamepad: true 29 | }; 30 | case KEYBOARD_ENABLED: 31 | return { 32 | ...state, 33 | keyboard: true 34 | }; 35 | case MOUSE_ENABLED: 36 | return { 37 | ...state, 38 | mouse: true 39 | }; 40 | case GAMEPAD_ENABLED: 41 | return { 42 | ...state, 43 | gamepad: true 44 | }; 45 | 46 | case INPUT_DISABLED: 47 | return { 48 | ...state, 49 | keyboard: false, 50 | mouse: false, 51 | gamepad: false 52 | }; 53 | case KEYBOARD_DISABLED: 54 | return { 55 | ...state, 56 | keyboard: false 57 | }; 58 | case MOUSE_DISABLED: 59 | return { 60 | ...state, 61 | mouse: false 62 | }; 63 | case GAMEPAD_DISABLED: 64 | return { 65 | ...state, 66 | gamepad: false 67 | }; 68 | 69 | case GAMEPAD_CONNECTED: 70 | case GAMEPAD_DISCONNECTED: 71 | return { 72 | ...state, 73 | gamepads: action.gamepads 74 | }; 75 | default: 76 | return state; 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /src/store/reducers/location.js: -------------------------------------------------------------------------------- 1 | import { LOCATION_PATH_CHANGE } from "../actions/types"; 2 | 3 | const DEFAULT_STATE = { 4 | path: '/' 5 | }; 6 | 7 | export default (state = DEFAULT_STATE, action = {}) => { 8 | switch(action.type) { 9 | case LOCATION_PATH_CHANGE: 10 | return { 11 | ...state, 12 | path: action.path 13 | }; 14 | default: 15 | return state; 16 | } 17 | } -------------------------------------------------------------------------------- /src/store/reducers/network.js: -------------------------------------------------------------------------------- 1 | import { 2 | NETWORK_CHANGED 3 | } from '../actions/types'; 4 | 5 | const DEFAULT_STATE = { 6 | isOnline: true 7 | }; 8 | 9 | export default (state = DEFAULT_STATE, action) => { 10 | switch(action.type) { 11 | case NETWORK_CHANGED: 12 | return { 13 | ...state, 14 | isOnline: action.isOnline 15 | } 16 | default: 17 | return state; 18 | } 19 | }; -------------------------------------------------------------------------------- /src/store/reducers/root.js: -------------------------------------------------------------------------------- 1 | import { 2 | RESET 3 | } from '../actions/types'; 4 | 5 | export const createRootReducer = (combinedReducer) => (state, action) => { 6 | switch (action.type) { 7 | case RESET: 8 | if (Object.keys(action.state).length > 0) { 9 | return { 10 | ...state, 11 | ...action.state 12 | }; 13 | } else { 14 | return state; 15 | } 16 | default: 17 | return combinedReducer(state, action); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/store/reducers/storage.js: -------------------------------------------------------------------------------- 1 | import { 2 | SAVE_STARTED, 3 | SAVE_ERROR, 4 | SAVE_COMPLETED, 5 | LOAD_STARTED, 6 | LOAD_COMPLETED, 7 | LOAD_ERROR 8 | } from '../actions/types'; 9 | 10 | const DEFAULT_STATE = { 11 | error: false, 12 | saving: false, 13 | loading: false, 14 | errorDetails: '', 15 | sceneName: '', 16 | currentPath: '/', 17 | timestamp: '' 18 | } 19 | 20 | export default (state = DEFAULT_STATE, action = {}) => { 21 | switch(action.type) { 22 | case SAVE_STARTED: 23 | return { 24 | ...state, 25 | saving: true 26 | }; 27 | case SAVE_COMPLETED: 28 | return { 29 | ...state, 30 | saving: false, 31 | error: false, 32 | sceneName: action.currentScene, 33 | currentPath: action.currentPath, 34 | timestamp: action.timestamp 35 | } 36 | case SAVE_ERROR: 37 | return { 38 | ...state, 39 | saving: false, 40 | error: true, 41 | errorDetails: action.errorDetails 42 | }; 43 | case LOAD_STARTED: 44 | return { 45 | ...state, 46 | loading: true 47 | }; 48 | case LOAD_COMPLETED: 49 | return { 50 | ...state, 51 | loading: false, 52 | error: false 53 | }; 54 | case LOAD_ERROR: 55 | return { 56 | ...state, 57 | loading: false, 58 | error: true, 59 | errorDetails: action.errorDetails 60 | } 61 | default: 62 | return state; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/store/reducers/ui.js: -------------------------------------------------------------------------------- 1 | import { 2 | UI_LOADING_SCREEN 3 | } from '../actions/types'; 4 | 5 | export const DEFAULT_STATE = { 6 | loadingScreenVisible: false 7 | }; 8 | 9 | export default (state = DEFAULT_STATE, action = {}) => { 10 | switch(action.type) { 11 | case UI_LOADING_SCREEN: 12 | return { 13 | ...state, 14 | loadingScreenVisible: action.loadingScreenVisible 15 | }; 16 | default: 17 | return state; 18 | } 19 | }; -------------------------------------------------------------------------------- /src/ui/BaseUI.js: -------------------------------------------------------------------------------- 1 | const BaseUI = () => null; 2 | 3 | export default BaseUI; 4 | -------------------------------------------------------------------------------- /src/ui/LabelComponent.js: -------------------------------------------------------------------------------- 1 | import { Component, createRef } from "inferno"; 2 | 3 | export default class LabelComponent extends Component { 4 | 5 | constructor(props) { 6 | super(props); 7 | this.element = createRef(); 8 | } 9 | 10 | componentDidUpdate() { 11 | this.props.onUpdate(this.element.current); 12 | } 13 | 14 | componentDidMount() { 15 | this.props.onMount(this.element.current); 16 | } 17 | 18 | componentWillUnmount() { 19 | this.props.onUnmount(this.element.current); 20 | } 21 | } -------------------------------------------------------------------------------- /src/ui/index.js: -------------------------------------------------------------------------------- 1 | import { render } from 'inferno'; 2 | import { createElement } from 'inferno-create-element'; 3 | import { Provider } from 'inferno-redux'; 4 | import { getStore } from '../store/Store'; 5 | import BaseUI from './BaseUI'; 6 | import Config from '../core/config'; 7 | import Router from '../router/Router'; 8 | import { dispatch } from '../store'; 9 | import { createElementFromSelector, removeElement } from '../lib/dom'; 10 | import { showLoadingScreen, hideLoadingScreen } from '../store/actions/ui'; 11 | import { locationPathChange } from '../store/actions/location'; 12 | 13 | const ROOT_ID = '#ui'; 14 | export const LABELS_ROOT_ID = '#labels_ui'; 15 | 16 | const createProps = () => ({ 17 | level: Router.getCurrentLevel() 18 | }); 19 | 20 | export const getUIContainer = (id = ROOT_ID) => { 21 | let rootElement = document.querySelector(id); 22 | 23 | if (!rootElement) { 24 | rootElement = createElementFromSelector(id); 25 | document.body.appendChild(rootElement); 26 | } 27 | 28 | return rootElement; 29 | }; 30 | 31 | export const requestLoadingScreen = () => dispatch(showLoadingScreen()); 32 | export const removeLoadingScreen = () => dispatch(hideLoadingScreen()); 33 | 34 | export const dispatchLocationPathChange = path => dispatch(locationPathChange(path)); 35 | 36 | export const mount = () => { 37 | const { root = BaseUI, enabled = true } = Config.ui(); 38 | if (!enabled) return; 39 | 40 | const store = getStore(); 41 | const uiElement = createElement(root, createProps()); 42 | 43 | if (store) { 44 | render( 45 | createElement(Provider, { 46 | store: getStore(), 47 | children: uiElement 48 | }), 49 | getUIContainer() 50 | ); 51 | } else { 52 | render(uiElement, getUIContainer()) 53 | } 54 | }; 55 | 56 | export const unmount = (id = ROOT_ID) => { 57 | const container = document.querySelector(id); 58 | 59 | // Rendering null will trigger unmount lifecycle hooks for whole vDOM tree and remove global event listeners. 60 | // https://github.com/infernojs/inferno#tear-down 61 | render(null, container); 62 | // removing the container as well 63 | removeElement(id); 64 | }; 65 | -------------------------------------------------------------------------------- /src/video/Video.js: -------------------------------------------------------------------------------- 1 | class Video { 2 | 3 | constructor() {} 4 | 5 | load() { return Promise.resolve(); } 6 | } 7 | 8 | const engine = new Video(); 9 | 10 | export default engine; 11 | --------------------------------------------------------------------------------