├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public └── index.html └── src ├── actions └── index.js ├── components ├── App.css ├── App.js ├── StartMessage.css ├── StartMessage.js ├── ThreeDisplay.css └── ThreeDisplay.js ├── containers ├── StartPage.js └── ThreeApp.js ├── index.js ├── reducers └── rootReducer.js └── threeApp ├── threeActionHelpers.js ├── threeApp.js └── threeHelpers.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## three.js - React - Redux 2 | 3 | Simple toy app built with [three.js](https://github.com/mrdoob/three.js), [React](https://github.com/facebook/react) and [Redux](https://github.com/reactjs/redux). 4 | 5 | This ist just a little experiment to figure out how to use Redux for mananging state and handling events in a three.js-based real-time 3D application. 6 | 7 | All three.js objects (scene, camera, renderer) live inside the ThreeApp container. This is the only place where mutations and side effects are supposed to happen. The scene gets updated every frame by `mapStateToScene()`. Similar functions could be used for applying state to the camera or selected parts of the three.js scene. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-three-test", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^15.6.1", 7 | "react-dom": "^15.6.1", 8 | "react-redux": "^5.0.6" 9 | }, 10 | "devDependencies": { 11 | "react-scripts": "1.0.11", 12 | "redux": "^3.7.2", 13 | "three": "^0.86.0" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test --env=jsdom", 19 | "eject": "react-scripts eject" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React, Redux & Three.js Test 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | export const fadeColor = e => { 2 | return { 3 | type: 'FADE_COLOR', 4 | e, 5 | } 6 | } 7 | 8 | export const switchColor = () => { 9 | return { 10 | type: 'SWITCH_COLOR', 11 | } 12 | } 13 | 14 | export const run = () => { 15 | return { 16 | type: 'RUN', 17 | } 18 | } 19 | 20 | export const update = timestamp => { 21 | return { 22 | type: 'UPDATE', 23 | timestamp, 24 | } 25 | } -------------------------------------------------------------------------------- /src/components/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | overflow: hidden; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import './App.css' 2 | 3 | import React from 'react' 4 | 5 | import StartPage from '../containers/StartPage' 6 | import ThreeApp from '../containers/ThreeApp' 7 | 8 | const App = props => ( 9 |
10 | 11 | 12 |
13 | ) 14 | 15 | export default App 16 | -------------------------------------------------------------------------------- /src/components/StartMessage.css: -------------------------------------------------------------------------------- 1 | .start-message { 2 | background-color: azure; 3 | color: skyblue; 4 | font-family: sans-serif; 5 | font-size: 2em; 6 | height: 100%; 7 | padding-top: 200px; 8 | position: absolute; 9 | text-align: center; 10 | width: 100%; 11 | } 12 | 13 | .hidden { 14 | visibility: hidden; 15 | } -------------------------------------------------------------------------------- /src/components/StartMessage.js: -------------------------------------------------------------------------------- 1 | import './StartMessage.css' 2 | 3 | import React from 'react' 4 | 5 | const StartMessage = ({ running, run }) => ( 6 |
7 |
11 | Click to start! 12 |
13 |
14 | ) 15 | 16 | export default StartMessage 17 | -------------------------------------------------------------------------------- /src/components/ThreeDisplay.css: -------------------------------------------------------------------------------- 1 | .three-display{ 2 | height: 100%; 3 | margin: 0; 4 | width: 100%; 5 | } 6 | 7 | .hidden { 8 | visibility: hidden; 9 | } -------------------------------------------------------------------------------- /src/components/ThreeDisplay.js: -------------------------------------------------------------------------------- 1 | import './ThreeDisplay.css' 2 | 3 | import React from 'react' 4 | import {connect} from 'react-redux' 5 | 6 | import {fadeColor, switchColor} from '../actions' 7 | 8 | class ThreeDisplay extends React.Component { 9 | shouldComponentUpdate(nextProps) { 10 | const shouldUpdate = nextProps.lastAction === 'RUN' || 'UPDATE' 11 | 12 | if (!shouldUpdate) { 13 | // This never gets logged, even if nextProps.lastAction is not 'RUN' or 'UPDATE' 14 | console.log('ThreeDisplay will not update') 15 | console.log('Last action: ' + nextProps.lastAction) 16 | } 17 | 18 | return shouldUpdate 19 | } 20 | 21 | componentWillUpdate() { 22 | // This gets logged even if this.props.lastAction is not 'RUN' or 'UPDATE 23 | console.log('ThreeDisplay will update') 24 | console.log('Last action: ' + this.props.lastAction) 25 | } 26 | 27 | render() { 28 | return ( 29 |
39 | ) 40 | } 41 | } 42 | 43 | const mapStateToProps = state => { 44 | return { 45 | running: state.running, 46 | lastAction: state.lastAction 47 | } 48 | } 49 | 50 | const mapDispatchTopProps = dispatch => { 51 | return { 52 | fadeColor: e => dispatch(fadeColor(e)), 53 | switchColor: () => dispatch(switchColor()) 54 | } 55 | } 56 | 57 | export default connect(mapStateToProps, mapDispatchTopProps)(ThreeDisplay) 58 | -------------------------------------------------------------------------------- /src/containers/StartPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | import { run } from '../actions' 5 | import StartMessage from '../components/StartMessage' 6 | 7 | const StartPage = ({ running, run }) => ( 8 |
9 | 13 |
14 | ) 15 | 16 | const mapStateToProps = (state) => { 17 | return { 18 | running: state.running, 19 | } 20 | } 21 | 22 | const mapDispatchTopProps = (dispatch) => { 23 | return { 24 | run: () => dispatch(run()), 25 | } 26 | } 27 | 28 | export default connect( 29 | mapStateToProps, 30 | mapDispatchTopProps 31 | )(StartPage) 32 | 33 | -------------------------------------------------------------------------------- /src/containers/ThreeApp.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {connect} from 'react-redux' 3 | 4 | import {update} from '../actions' 5 | 6 | import ThreeDisplay from '../components/ThreeDisplay' 7 | 8 | import { 9 | getThreeCamera, 10 | getThreeRenderer, 11 | getThreeScene 12 | } from '../threeApp/threeApp' 13 | 14 | import {mapStateToScene} from '../threeApp/threeHelpers' 15 | 16 | class ThreeApp extends React.Component { 17 | renderNextFrame = () => { 18 | this.threeRenderer.render(this.scene, this.camera) 19 | requestAnimationFrame(timestamp => this.props.update(timestamp)) 20 | } 21 | 22 | componentDidMount() { 23 | this.scene = getThreeScene() 24 | this.camera = getThreeCamera() 25 | this.threeRenderer = getThreeRenderer() 26 | 27 | this.renderNextFrame() 28 | } 29 | 30 | shouldComponentUpdate(nextProps) { 31 | return nextProps.lastAction === 'UPDATE' 32 | } 33 | 34 | componentWillUpdate() { 35 | mapStateToScene(this.props.sceneState, this.scene) 36 | this.renderNextFrame() 37 | } 38 | 39 | render() { 40 | return ( 41 |
42 | 43 |
44 | ) 45 | } 46 | } 47 | 48 | const mapStateToProps = state => { 49 | return { 50 | timestamp: state.timestamp, 51 | lastAction: state.lastAction, 52 | sceneState: state.scene 53 | } 54 | } 55 | 56 | const mapDispatchTopProps = dispatch => { 57 | return { 58 | update: timestamp => dispatch(update(timestamp)) 59 | } 60 | } 61 | 62 | export default connect(mapStateToProps, mapDispatchTopProps)(ThreeApp) 63 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { Provider } from 'react-redux' 4 | import { createStore } from 'redux' 5 | 6 | import App from './components/App' 7 | import rootReducer from './reducers/rootReducer' 8 | import { getInitialSceneState } from './threeApp/threeApp' 9 | 10 | const threeInitialState = getInitialSceneState(); 11 | 12 | const initialState = { 13 | running: false, 14 | timestamp: 0, 15 | lastAction: '', 16 | scene: threeInitialState 17 | } 18 | 19 | const store = createStore(rootReducer, initialState) 20 | 21 | render( 22 | 23 | 24 | , 25 | document.getElementById('root') 26 | ) -------------------------------------------------------------------------------- /src/reducers/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { switchColor, fadeColor, updatePosition } from '../threeApp/threeActionHelpers' 2 | 3 | const rootReducer = (state, action) => { 4 | switch (action.type) { 5 | case 'FADE_COLOR': { 6 | const newState = fadeColor(state, action.e) 7 | return { 8 | ...newState, 9 | lastAction: 'FADE_COLOR' 10 | } 11 | } 12 | case 'SWITCH_COLOR': { 13 | const newState = switchColor(state) 14 | return { 15 | ...newState, 16 | lastAction: 'SWITCH_COLOR' 17 | } 18 | } 19 | case 'RUN': 20 | return { 21 | ...state, 22 | running: true, 23 | lastAction: 'RUN', 24 | } 25 | case 'UPDATE': { 26 | const newState = { 27 | ...state, 28 | timestamp: action.timestamp, 29 | lastAction: 'UPDATE', 30 | } 31 | return updatePosition(newState) 32 | } 33 | default: 34 | return state 35 | } 36 | } 37 | 38 | export default rootReducer; -------------------------------------------------------------------------------- /src/threeApp/threeActionHelpers.js: -------------------------------------------------------------------------------- 1 | export const fadeColor = (state, e) => { 2 | const newState = { ...state } 3 | const newColors = state.scene.spheres.colors.map((color) => { 4 | color.r = e.nativeEvent.clientX / window.innerWidth 5 | color.b = e.nativeEvent.clientY / window.innerHeight 6 | return color 7 | }) 8 | newState.colors = newColors 9 | return newState 10 | } 11 | 12 | export const switchColor = (state) => { 13 | const newState = { ...state } 14 | const newColors = state.scene.spheres.colors.map((color) => { 15 | color.g = Math.random() * 0.5 + 0.5 16 | return color 17 | }) 18 | newState.colors = newColors 19 | return newState 20 | } 21 | 22 | export const updatePosition = (state) => { 23 | const newState = { ...state } 24 | const newPositions = state.scene.spheres.positions.map((position) => { 25 | position.z = 2 * Math.sin(state.timestamp / 1000 + (position.x / 10) + (position.y / 10)) 26 | return position 27 | }) 28 | newState.scene.spheres.positions = newPositions 29 | return newState 30 | } -------------------------------------------------------------------------------- /src/threeApp/threeApp.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | export const getThreeCamera = () => { 4 | const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000) 5 | camera.position.x = 40 6 | camera.position.y = 40 7 | camera.position.z = 60 8 | 9 | return camera 10 | } 11 | 12 | export const getInitialSceneState = () => { 13 | 14 | const initialState = { 15 | spheres: { 16 | colors: [], 17 | positions: [], 18 | }, 19 | } 20 | 21 | for (let y = 0; y < 20; y++) { 22 | for (let x = 0; x < 20; x++) { 23 | const positionX = x * 4 24 | const positionY = y * 4 25 | initialState.spheres.positions.push({ x: positionX, y: positionY, z: 0 }) 26 | 27 | initialState.spheres.colors.push({r: 1, g: 0.65, b: 0}) 28 | } 29 | } 30 | return initialState 31 | } 32 | 33 | export const getThreeRenderer = () => { 34 | const container = document.getElementById('container') 35 | const renderer = new THREE.WebGLRenderer({ antialias: true }) 36 | renderer.setClearColor(0xf0ffff) 37 | renderer.setSize(window.innerWidth, window.innerHeight) 38 | container.appendChild(renderer.domElement) 39 | 40 | return renderer 41 | } 42 | 43 | export const getThreeScene = () => { 44 | const scene = new THREE.Scene() 45 | 46 | for (let y = 0; y < 20; y++) { 47 | for (let x = 0; x < 20; x++) { 48 | const geometry = new THREE.SphereGeometry(1, 32, 32) 49 | const material = new THREE.MeshBasicMaterial({ color: 0xffa500 }) 50 | const sphere = new THREE.Mesh(geometry, material) 51 | sphere.position.x = x * 4 52 | sphere.position.y = y * 4 53 | sphere.name = x + (y * 20) 54 | scene.add(sphere) 55 | } 56 | } 57 | return scene 58 | } 59 | -------------------------------------------------------------------------------- /src/threeApp/threeHelpers.js: -------------------------------------------------------------------------------- 1 | export const mapStateToScene = (sceneState, scene) => { 2 | for (let i = 0; i < sceneState.spheres.positions.length; i++) { 3 | const mesh = scene.getObjectByName(i) 4 | const color = sceneState.spheres.colors[i] 5 | const position = sceneState.spheres.positions[i] 6 | mesh.material.color = color 7 | mesh.position.set(position.x, position.y, position.z) 8 | } 9 | } --------------------------------------------------------------------------------