├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── assets │ └── logo.png ├── components │ ├── ControlPanel.vue │ └── ViewPort.vue ├── main.js ├── router.js ├── store.js └── views │ ├── About.vue │ └── Home.vue └── tests └── unit ├── .eslintrc.js └── example.spec.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: ["plugin:vue/essential", "@vue/prettier"], 7 | rules: { 8 | "no-console": process.env.NODE_ENV === "production" ? "error" : "off", 9 | "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off" 10 | }, 11 | parserOptions: { 12 | parser: "babel-eslint" 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ryan Durham 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-threejs-demo 2 | 3 | This is a Proof-Of-Concept integration of Vue.js and Three.js. I have converted the Three.js [Trackball Control Example](https://threejs.org/examples/?q=cont#misc_controls_trackball) to a Vue/Vuex application and added some additional UI sugar on top. 4 | 5 | [See the demo here](https://vuethree.stagerightlabs.com/) 6 | 7 | ## Project setup 8 | ``` 9 | npm install 10 | ``` 11 | 12 | ### Compiles and hot-reloads for development 13 | ``` 14 | npm run serve 15 | ``` 16 | 17 | ### Compiles and minifies for production 18 | ``` 19 | npm run build 20 | ``` 21 | 22 | 23 | ## Helpful Links 24 | 25 | https://stackoverflow.com/questions/47849626/import-and-use-three-js-library-in-vue-component 26 | 27 | https://www.reddit.com/r/vuejs/comments/7fdq9b/vue_and_threejs/ 28 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/app"] 3 | }; 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ["js", "jsx", "json", "vue"], 3 | transform: { 4 | "^.+\\.vue$": "vue-jest", 5 | ".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$": 6 | "jest-transform-stub", 7 | "^.+\\.jsx?$": "babel-jest" 8 | }, 9 | transformIgnorePatterns: ["/node_modules/"], 10 | moduleNameMapper: { 11 | "^@/(.*)$": "/src/$1" 12 | }, 13 | snapshotSerializers: ["jest-serializer-vue"], 14 | testMatch: [ 15 | "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)" 16 | ], 17 | testURL: "http://localhost/" 18 | }; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-threejs-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint", 9 | "test:unit": "vue-cli-service test:unit" 10 | }, 11 | "dependencies": { 12 | "three-full": "^17.1.0", 13 | "vue": "^3.0.0", 14 | "vue-router": "^3.0.1", 15 | "vuex": "^3.0.1" 16 | }, 17 | "devDependencies": { 18 | "@vue/cli-plugin-babel": "^5.0.8", 19 | "@vue/cli-plugin-eslint": "^5.0.8", 20 | "@vue/cli-plugin-unit-jest": "^5.0.8", 21 | "@vue/cli-service": "^5.0.8", 22 | "@vue/eslint-config-prettier": "^4.0.1", 23 | "@vue/test-utils": "1.0.0-beta.29", 24 | "babel-core": "7.0.0-bridge.0", 25 | "babel-eslint": "^10.1.0", 26 | "babel-jest": "^29.7.0", 27 | "eslint": "^5.16.0", 28 | "eslint-plugin-vue": "^5.2.3", 29 | "vue-template-compiler": "^2.6.12" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagerightlabs/Vue-Three-Demo/cb94bcf80dee863c24ae8487a0459e205a413dbe/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-threejs-demo 9 | 10 | 15 | 16 | 17 | 20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | 20 | 37 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagerightlabs/Vue-Three-Demo/cb94bcf80dee863c24ae8487a0459e205a413dbe/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/ControlPanel.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 125 | -------------------------------------------------------------------------------- /src/components/ViewPort.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 39 | 40 | 46 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | import router from "./router"; 4 | import store from "./store"; 5 | 6 | Vue.config.productionTip = false; 7 | 8 | new Vue({ 9 | router, 10 | store, 11 | render: h => h(App) 12 | }).$mount("#app"); 13 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | // import Vue from "vue"; 2 | // import Router from "vue-router"; 3 | 4 | // Vue.use(Router); 5 | 6 | // export default new Router({ 7 | // mode: "history", 8 | // base: process.env.BASE_URL, 9 | // routes: [] 10 | // }); 11 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex from "vuex"; 3 | import { 4 | Scene, 5 | TrackballControls, 6 | PerspectiveCamera, 7 | WebGLRenderer, 8 | Color, 9 | FogExp2, 10 | CylinderBufferGeometry, 11 | MeshPhongMaterial, 12 | Mesh, 13 | DirectionalLight, 14 | AmbientLight, 15 | LineBasicMaterial, 16 | Geometry, 17 | Vector3, 18 | Line 19 | } from "three-full"; 20 | 21 | Vue.use(Vuex); 22 | 23 | export default new Vuex.Store({ 24 | state: { 25 | width: 0, 26 | height: 0, 27 | camera: null, 28 | controls: null, 29 | scene: null, 30 | renderer: null, 31 | axisLines: [], 32 | pyramids: [] 33 | }, 34 | getters: { 35 | CAMERA_POSITION: state => { 36 | return state.camera ? state.camera.position : null; 37 | } 38 | }, 39 | mutations: { 40 | SET_VIEWPORT_SIZE(state, { width, height }) { 41 | state.width = width; 42 | state.height = height; 43 | }, 44 | INITIALIZE_RENDERER(state, el) { 45 | state.renderer = new WebGLRenderer({ antialias: true }); 46 | state.renderer.setPixelRatio(window.devicePixelRatio); 47 | state.renderer.setSize(state.width, state.height); 48 | el.appendChild(state.renderer.domElement); 49 | }, 50 | INITIALIZE_CAMERA(state) { 51 | state.camera = new PerspectiveCamera( 52 | // 1. Field of View (degrees) 53 | 60, 54 | // 2. Aspect ratio 55 | state.width / state.height, 56 | // 3. Near clipping plane 57 | 1, 58 | // 4. Far clipping plane 59 | 1000 60 | ); 61 | state.camera.position.z = 500; 62 | }, 63 | INITIALIZE_CONTROLS(state) { 64 | state.controls = new TrackballControls( 65 | state.camera, 66 | state.renderer.domElement 67 | ); 68 | state.controls.rotateSpeed = 1.0; 69 | state.controls.zoomSpeed = 1.2; 70 | state.controls.panSpeed = 0.8; 71 | state.controls.noZoom = false; 72 | state.controls.noPan = false; 73 | state.controls.staticMoving = true; 74 | state.controls.dynamicDampingFactor = 0.3; 75 | state.controls.keys = [65, 83, 68]; 76 | }, 77 | UPDATE_CONTROLS(state) { 78 | state.controls.update(); 79 | }, 80 | INITIALIZE_SCENE(state) { 81 | state.scene = new Scene(); 82 | state.scene.background = new Color(0xcccccc); 83 | state.scene.fog = new FogExp2(0xcccccc, 0.002); 84 | var geometry = new CylinderBufferGeometry(0, 10, 30, 4, 1); 85 | var material = new MeshPhongMaterial({ 86 | color: 0xffffff, 87 | flatShading: true 88 | }); 89 | for (var i = 0; i < 500; i++) { 90 | var mesh = new Mesh(geometry, material); 91 | mesh.position.x = (Math.random() - 0.5) * 1000; 92 | mesh.position.y = (Math.random() - 0.5) * 1000; 93 | mesh.position.z = (Math.random() - 0.5) * 1000; 94 | mesh.updateMatrix(); 95 | mesh.matrixAutoUpdate = false; 96 | state.pyramids.push(mesh); 97 | } 98 | state.scene.add(...state.pyramids); 99 | 100 | // lights 101 | var lightA = new DirectionalLight(0xffffff); 102 | lightA.position.set(1, 1, 1); 103 | state.scene.add(lightA); 104 | var lightB = new DirectionalLight(0x002288); 105 | lightB.position.set(-1, -1, -1); 106 | state.scene.add(lightB); 107 | var lightC = new AmbientLight(0x222222); 108 | state.scene.add(lightC); 109 | 110 | // Axis Line 1 111 | var materialB = new LineBasicMaterial({ color: 0x0000ff }); 112 | var geometryB = new Geometry(); 113 | geometryB.vertices.push(new Vector3(0, 0, 0)); 114 | geometryB.vertices.push(new Vector3(0, 1000, 0)); 115 | var lineA = new Line(geometryB, materialB); 116 | state.axisLines.push(lineA); 117 | 118 | // Axis Line 2 119 | var materialC = new LineBasicMaterial({ color: 0x00ff00 }); 120 | var geometryC = new Geometry(); 121 | geometryC.vertices.push(new Vector3(0, 0, 0)); 122 | geometryC.vertices.push(new Vector3(1000, 0, 0)); 123 | var lineB = new Line(geometryC, materialC); 124 | state.axisLines.push(lineB); 125 | 126 | // Axis 3 127 | var materialD = new LineBasicMaterial({ color: 0xff0000 }); 128 | var geometryD = new Geometry(); 129 | geometryD.vertices.push(new Vector3(0, 0, 0)); 130 | geometryD.vertices.push(new Vector3(0, 0, 1000)); 131 | var lineC = new Line(geometryD, materialD); 132 | state.axisLines.push(lineC); 133 | 134 | state.scene.add(...state.axisLines); 135 | }, 136 | RESIZE(state, { width, height }) { 137 | state.width = width; 138 | state.height = height; 139 | state.camera.aspect = width / height; 140 | state.camera.updateProjectionMatrix(); 141 | state.renderer.setSize(width, height); 142 | state.controls.handleResize(); 143 | state.renderer.render(state.scene, state.camera); 144 | }, 145 | SET_CAMERA_POSITION(state, { x, y, z }) { 146 | if (state.camera) { 147 | state.camera.position.set(x, y, z); 148 | } 149 | }, 150 | RESET_CAMERA_ROTATION(state) { 151 | if (state.camera) { 152 | state.camera.rotation.set(0, 0, 0); 153 | state.camera.quaternion.set(0, 0, 0, 1); 154 | state.camera.up.set(0, 1, 0); 155 | state.controls.target.set(0, 0, 0); 156 | } 157 | }, 158 | HIDE_AXIS_LINES(state) { 159 | state.scene.remove(...state.axisLines); 160 | state.renderer.render(state.scene, state.camera); 161 | }, 162 | SHOW_AXIS_LINES(state) { 163 | state.scene.add(...state.axisLines); 164 | state.renderer.render(state.scene, state.camera); 165 | }, 166 | HIDE_PYRAMIDS(state) { 167 | state.scene.remove(...state.pyramids); 168 | state.renderer.render(state.scene, state.camera); 169 | }, 170 | SHOW_PYRAMIDS(state) { 171 | state.scene.add(...state.pyramids); 172 | state.renderer.render(state.scene, state.camera); 173 | } 174 | }, 175 | actions: { 176 | INIT({ state, commit }, { width, height, el }) { 177 | return new Promise(resolve => { 178 | commit("SET_VIEWPORT_SIZE", { width, height }); 179 | commit("INITIALIZE_RENDERER", el); 180 | commit("INITIALIZE_CAMERA"); 181 | commit("INITIALIZE_CONTROLS"); 182 | commit("INITIALIZE_SCENE"); 183 | 184 | // Initial scene rendering 185 | state.renderer.render(state.scene, state.camera); 186 | 187 | // Add an event listener that will re-render 188 | // the scene when the controls are changed 189 | state.controls.addEventListener("change", () => { 190 | state.renderer.render(state.scene, state.camera); 191 | }); 192 | 193 | resolve(); 194 | }); 195 | }, 196 | ANIMATE({ state, dispatch }) { 197 | window.requestAnimationFrame(() => { 198 | dispatch("ANIMATE"); 199 | state.controls.update(); 200 | }); 201 | } 202 | } 203 | }); 204 | -------------------------------------------------------------------------------- /src/views/About.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /tests/unit/example.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from "@vue/test-utils"; 2 | import HelloWorld from "@/components/HelloWorld.vue"; 3 | 4 | describe("HelloWorld.vue", () => { 5 | it("renders props.msg when passed", () => { 6 | const msg = "new message"; 7 | const wrapper = shallowMount(HelloWorld, { 8 | propsData: { msg } 9 | }); 10 | expect(wrapper.text()).toMatch(msg); 11 | }); 12 | }); 13 | --------------------------------------------------------------------------------